想要实现SpringSecurity的认证授权,首先需要理解大致流程。
- 导入maven依赖
org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt 0.9.0
- 创建数据库表
表数据如下:
- 创建生成jwtToken的工具类
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; @Component public class JwtTokenUtil { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; private String generateToken(Map实现 实现思路claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { LOGGER.info("JWT格式验证失败:{}",token); } return claims; } private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } public String getUserNameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } public String generateToken(UserDetails userDetails) { Map claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } public boolean canRefresh(String token) { return !isTokenExpired(token); } public String refreshToken(String token) { Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } }
- 实现UserDetailsService对象中的loadUserByUsername方法,根据用户名查询用户信息以及权限信息;
- 自定义controller登录接口;
- 自定义service层登录逻辑,调用loadUserByUsername获取用户信息并且校验密码,如果认证成功则生成一个jwt返回,将用户信息存入SecurityContextHolder上下文;
- 自定义JwtAuthenticationTokenFilter过滤器,获取请求头中的token,校验token的有效性并从中获取username,调用loadUserByUsername获取用户信息,存入SecurityContextHolder上下文。
这里为什么要将用户信息及权限存入SecurityContextHolder上下文中?
SecurityContextHolder基于ThreadLocal,SpringSecurity的底层就是一个过滤器链,并且这些过滤器都是一条线执行的。
而权限的验证主要就是在FilterSecurityInterceptor过滤器中执行的,将用户权限信息存入SecurityContextHolder上下文中,就是为了使后面的过滤器在执行相关权限验证的功能时能够获取到用户的权限信息。
@Configuration @EnableWebSecurity // 开启Security @EnableGlobalMethodSecurity(prePostEnabled=true) // 开启权限功能 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private RestAuthenticationEntryPoint restAuthenticationEntryPoint; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Autowired private UserMapper userMapper; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf()// 由于使用的是JWT,我们这里不需要csrf .disable() .sessionManagement()// 基于token,所以不需要session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问 "/", "*.html", "*.css", "*.js", "/swagger-resources @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){ return new JwtAuthenticationTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
RestAuthenticationEntryPointUserDetailsService:注册该组件并实现核心逻辑方法,根据传来用户名查询该用户的信息。
PasswordEncoder:指定加密方式为BCryptPasswordEncoder,不指定则使用默认的明文存储密码的方式。
JwtAuthenticationTokenFilter:jwt登录授权过滤器,用于验证请求头携带token的是否有效。
方法详细见下文。
当未登录或者token失效访问接口时,自定义的返回结果。
@Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(R.error(10002, "无效的token")); response.getWriter().flush(); } }RestfulAccessDeniedHandler
当访问接口没有权限时,自定义的返回结果。
@Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler{ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(R.error(10003, "无权限访问")); response.getWriter().flush(); } }JwtAuthenticationTokenFilter
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; // 请求头名: token @Value("${jwt.tokenHead}") private String tokenHead; // jwt中的header部分 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 获取请求头中的token String authToken = request.getHeader(this.tokenHeader); if (authToken != null && authToken.startsWith(this.tokenHead)) { // 从token中获取username String username = jwtTokenUtil.getUserNameFromToken(authToken); LOGGER.info("checking username:{}", username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 验证该token是否有效 if (jwtTokenUtil.validateToken(authToken, userDetails)) { // 将 userDetails 封装为一个 authentication 对象 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); LOGGER.info("authenticated user:{}", username); // 存储 authentication对象 至SecurityContextHolder上下文中 SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }UserAuthDetails(UserDetails )对象
用户信息以及对应的权限信息都封装在该对象中,loadUserByUsername方法的返回值。
public class UserAuthDetails implements UserDetails { private User user; private ListController
登录接口以及两个需要权限访问的业务功能,以便后续测试。
@Slf4j @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public R login(@RequestBody User user) { String token = null; try { token = userService.login(user); } catch (Exception e) { return R.error(10001, e.getMessage()); } log.info("the token is created: {}", token); return R.ok().setData(token); } @GetMapping("/list") @PreAuthorize("hasAuthority('list')") public R list() { return R.ok(); } @DeleteMapping("/delete") @PreAuthorize("hasAuthority('delete')") public R delete() { return R.ok(); } }UserService接口及实现类
public interface UserService { ListgetPermsByUserId(Integer userId); String login(User user); }
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private JwtTokenUtil jwtTokenUtil; @Override public ListgetPermsByUserId(Integer userId) { return userMapper.getPermsByUserId(userId); } @Override public String login(User user) { String token = null; String username = user.getUsername(); String password = user.getPassword(); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (!Objects.isNull(userDetails)) { // 校验密码 boolean matches = passwordEncoder.matches(password, userDetails.getPassword()); if (!matches) { throw new PasswordNotMatchException("用户名或密码错误"); } // 将UserDetails对象封装到authentication对象中 并保存至SecurityContextHolder上下文 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); // 生成token返回 token = jwtTokenUtil.generateToken(userDetails); } return token; } }
重点查看login方法,上述代码手动调用了loadUserByUsername方法与matches方法校验密码,其实还有一种简单的方式,从开头的流程图中可以发现,在调用ProviderManager对象的authenticate方法时,他在内部已经帮我们调用了loadUserByUsername方法和matches方法进行用户的认证,并且在认证成功之后会返回一个Authentication,该对象封装了UserDetails 对象,所以我们也可以直接调用authenticate方法进行认证。
@Autowired private AuthenticationManager authenticationManager; @Override public String login(User user) { String token = null; String username = user.getUsername(); String password = user.getPassword(); // 将用户名和密码封装为一个Authentication对象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); // 调用认证方法 Authentication authenticate = authenticationManager.authenticate(authenticationToken); if (Objects.isNull(authenticate)) { throw new PasswordNotMatchException("用户名或密码错误"); } UserAuthDetails userDetails = (UserAuthDetails) authenticate.getPrincipal(); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); // 生成token返回 token = jwtTokenUtil.generateToken(userDetails); return token; }测试
运行项目,使用postman工具进行测试:
使用user角色的用户xiaoming,进行登录。
认证测试
- 输入错误用户名:
- 输入错误密码:
- 登录成功:
登录成功,返回jwt token给客户端。
权限测试
该user权限只能访问list权限的控制器**
- 携带正确的token访问**/user/list**
- 携带错误的token访问**/user/list**:
- 访问无权限的接口**/user/detele**: