《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/larger5/article/details/81063438 「larger5」欢迎转载,保留摘要,谢谢!


一、前言

修改自前文,十分贴近公司开发的生产环境

前后端分离 SpringBoot + SpringSecurity 权限解决方案

RBAC(Role-Based Access Control,基于角色的访问控制)

二、代码

代码已经放在 github 上了:https://github.com/larger5/SpringBoot_SpringSecurity_JWT_RBAC.git

1. pom

<!--安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--JSON封装-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.36</version>
</dependency>

<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

2. AjaxResponseBody

package com.cun.security3.bean;

import java.io.Serializable;

public class AjaxResponseBody implements Serializable{

private String status;
private String msg;
private Object result;
private String jwtToken;

public String getStatus() {
return status;
}

public void setStatus(String status) {
this.status = status;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Object getResult() {
return result;
}

public void setResult(Object result) {
this.result = result;
}

public String getJwtToken() {
return jwtToken;
}

public void setJwtToken(String jwtToken) {
this.jwtToken = jwtToken;
}
}

3. AjaxAccessDeniedHandler

package com.cun.security3.config;

import com.alibaba.fastjson.JSON;
import com.cun.security3.bean.AjaxResponseBody;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
AjaxResponseBody responseBody = new AjaxResponseBody();

responseBody.setStatus("300");
responseBody.setMsg("Need Authorities!");

httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
}

4. AjaxAuthenticationEntryPoint

package com.cun.security3.config;

import com.alibaba.fastjson.JSON;
import com.cun.security3.bean.AjaxResponseBody;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
AjaxResponseBody responseBody = new AjaxResponseBody();

responseBody.setStatus("000");
responseBody.setMsg("Need Authorities!");

httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
}

5. AjaxAuthenticationFailureHandler

package com.cun.security3.config;

import com.alibaba.fastjson.JSON;
import com.cun.security3.bean.AjaxResponseBody;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
AjaxResponseBody responseBody = new AjaxResponseBody();

responseBody.setStatus("400");
responseBody.setMsg("Login Failure!");

httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
}

6. AjaxAuthenticationSuccessHandler

package com.cun.security3.config;

import com.alibaba.fastjson.JSON;
import com.cun.security3.bean.AjaxResponseBody;
import com.cun.security3.utils.JwtTokenUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
AjaxResponseBody responseBody = new AjaxResponseBody();

responseBody.setStatus("200");
responseBody.setMsg("Login Success!");

SelfUserDetails userDetails = (SelfUserDetails) authentication.getPrincipal();

String jwtToken = JwtTokenUtil.generateToken(userDetails.getUsername(), 300, "_secret");
responseBody.setJwtToken(jwtToken);

httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
}

7. AjaxLogoutSuccessHandler

package com.cun.security3.config;

import com.alibaba.fastjson.JSON;
import com.cun.security3.bean.AjaxResponseBody;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler {

@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
AjaxResponseBody responseBody = new AjaxResponseBody();

responseBody.setStatus("100");
responseBody.setMsg("Logout Success!");

httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
}

8. JwtAuthenticationTokenFilter

package com.cun.security3.config;

import com.cun.security3.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired
SelfUserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");

if (authHeader != null && authHeader.startsWith("Bearer ")) {
final String authToken = authHeader.substring("Bearer ".length());

String username = JwtTokenUtil.parseToken(authToken, "_secret");

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}

chain.doFilter(request, response);
}
}

9. RbacAuthorityService

package com.cun.security3.config;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;

@Component("rbacauthorityservice")
public class RbacAuthorityService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {

Object userInfo = authentication.getPrincipal();

boolean hasPermission = false;

if (userInfo instanceof UserDetails) {

String username = ((UserDetails) userInfo).getUsername();

//获取资源
Set<String> urls = new HashSet();
urls.add("/common/**"); // 这些 url 都是要登录后才能访问,且其他的 url 都不能访问!
Set set2 = new HashSet();
Set set3 = new HashSet();

AntPathMatcher antPathMatcher = new AntPathMatcher();

for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}

return hasPermission;
} else {
return false;
}
}
}

10. SelfUserDetails

package com.cun.security3.config;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.Set;

/**
* ① 定义 user 对象
*/
public class SelfUserDetails implements UserDetails, Serializable {
private String username;
private String password;
private Set<? extends GrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

public void setAuthorities(Set<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}

@Override
public String getPassword() { // 最重点Ⅰ
return this.password;
}

@Override
public String getUsername() { // 最重点Ⅱ
return this.username;
}

public void setUsername(String username) {
this.username = username;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

11. SelfUserDetailsService

package com.cun.security3.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;

@Component
public class SelfUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//构建用户信息的逻辑(取数据库/LDAP等用户信息)

SelfUserDetails userInfo = new SelfUserDetails();
userInfo.setUsername(username);
userInfo.setPassword(new BCryptPasswordEncoder().encode("123"));

Set authoritiesSet = new HashSet();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");
authoritiesSet.add(authority);
userInfo.setAuthorities(authoritiesSet);

return userInfo;
}
}

12. SpringSecurityConf

package com.cun.security3.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SpringSecurityConf extends WebSecurityConfigurerAdapter {

@Autowired
AjaxAuthenticationEntryPoint authenticationEntryPoint; // 未登录时返回 JSON 格式的数据给前端(否则为 html)

@Autowired
AjaxAuthenticationSuccessHandler authenticationSuccessHandler; // 登录成功返回的 JSON 格式数据给前端(否则为 html)

@Autowired
AjaxAuthenticationFailureHandler authenticationFailureHandler; // 登录失败返回的 JSON 格式数据给前端(否则为 html)

@Autowired
AjaxLogoutSuccessHandler logoutSuccessHandler; // 注销成功返回的 JSON 格式数据给前端(否则为 登录时的 html)

@Autowired
AjaxAccessDeniedHandler accessDeniedHandler; // 无权访问返回的 JSON 格式数据给前端(否则为 403 html 页面)

@Autowired
SelfUserDetailsService userDetailsService; // 自定义user

@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 拦截器

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

// 加入自定义的安全认证
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}

@Override
protected void configure(HttpSecurity http) throws Exception {

// 去掉 CSRF
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 使用 JWT,关闭token
.and()

.httpBasic().authenticationEntryPoint(authenticationEntryPoint)

.and()
.authorizeRequests()

.anyRequest()
.access("@rbacauthorityservice.hasPermission(request,authentication)") // RBAC 动态 url 认证

.and()
.formLogin() //开启登录
.successHandler(authenticationSuccessHandler) // 登录成功
.failureHandler(authenticationFailureHandler) // 登录失败
.permitAll()

.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.permitAll();

// 记住我
http.rememberMe().rememberMeParameter("remember-me")
.userDetailsService(userDetailsService).tokenValiditySeconds(300);

http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 无权访问 JSON 格式的数据
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // JWT Filter

}
}

13. JwtTokenUtil

package com.cun.security3.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.io.InputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;

public class JwtTokenUtil {

private static InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("jwt.jks"); // 寻找证书文件
private static PrivateKey privateKey = null;
private static PublicKey publicKey = null;

static { // 将证书文件里边的私钥公钥拿出来
try {
KeyStore keyStore = KeyStore.getInstance("JKS"); // java key store 固定常量
keyStore.load(inputStream, "123456".toCharArray());
privateKey = (PrivateKey) keyStore.getKey("jwt", "123456".toCharArray()); // jwt 为 命令生成整数文件时的别名
publicKey = keyStore.getCertificate("jwt").getPublicKey();
} catch (Exception e) {
e.printStackTrace();
}
}

public static String generateToken(String subject, int expirationSeconds, String salt) {
return Jwts.builder()
.setClaims(null)
.setSubject(subject)
.setExpiration(new Date(System.currentTimeMillis() + expirationSeconds * 1000))
// .signWith(SignatureAlgorithm.HS512, salt) // 不使用公钥私钥
.signWith(SignatureAlgorithm.RS256, privateKey)
.compact();
}

public static String parseToken(String token, String salt) {
String subject = null;
try {
Claims claims = Jwts.parser()
// .setSigningKey(salt) // 不使用公钥私钥
.setSigningKey(publicKey)
.parseClaimsJws(token).getBody();
subject = claims.getSubject();
} catch (Exception e) {
}
return subject;
}

}

三、其他

不足 解决
没有注销 token 登录 每次登录,生成 token 放到 Redis 数据库里边,调用接口的时候,先查有没有这个 token,注销时把 token 删除

四、测试示例(2018/8/29更新)

由于网友对 jwt 的使用方式存在疑问,这里更新一下测试方法, 借助了 postman,每次请求都是 ajax 方式,注意 get/post 等类型

1. 登录示例

①注意是 post 请求,请求 /login,存好返回给你的 jwtToken,以后每次请求都要带上它

这里写图片描述

2. 访问内部示例

① 注意任何请求要带上 jwtToken,不像之前的基于 Session,一登录成功就完事。

② 若还是请求失败,看 RbacAuthorityService 是否开放了该 url

这里写图片描述

五、关于 Token(2019.9.13更新)

实际开发中,没有必要引入 jjwt,token 存于 Redis 即可(自带有效期),性能更好。

文章目录
  1. 1. 一、前言
  2. 2. 二、代码
    1. 2.1. 1. pom
    2. 2.2. 2. AjaxResponseBody
    3. 2.3. 3. AjaxAccessDeniedHandler
    4. 2.4. 4. AjaxAuthenticationEntryPoint
    5. 2.5. 5. AjaxAuthenticationFailureHandler
    6. 2.6. 6. AjaxAuthenticationSuccessHandler
    7. 2.7. 7. AjaxLogoutSuccessHandler
    8. 2.8. 8. JwtAuthenticationTokenFilter
    9. 2.9. 9. RbacAuthorityService
    10. 2.10. 10. SelfUserDetails
    11. 2.11. 11. SelfUserDetailsService
    12. 2.12. 12. SpringSecurityConf
    13. 2.13. 13. JwtTokenUtil
  3. 3. 三、其他
  4. 4. 四、测试示例(2018/8/29更新)
    1. 4.1. 1. 登录示例
    2. 4.2. 2. 访问内部示例
  5. 5. 五、关于 Token(2019.9.13更新)