赞
踩
SpringSecurity基于表单的登录认证流程如下图:
大致过程是在UsernamePasswordAuthenticationFilter将username和password封装成UsernamePasswordAuthenticationToken,之后会在AuthenticationProvider(接口,需要自己实现)的authenticate方法中拿到且校验(一般是查数据库,对比账号密码),而校验成功则返回Authentication接口对象(一般情况是直接返回UsernamePasswordAuthenticationToken,该对象会默认记录在session以及SecurityContextHolder的SecurityContext中)随后走认证成功流程(一般会实现AuthenticationSuccessHandler接口),否则就抛异常走认证失败流程(一般会实现AuthenticationFailureHandler接口)。
以上是普通单体项目的认证过程,那么整合JWT或者说微服务则会有以下问题:
对于第一个问题,很好解决,在AuthenticationProvider认证成功后不返回UsernamePasswordAuthenticationToken,返回自定义的JwtAuthenticationToken,然后在自定义的LoginSuccessHandler返回给前端即可,代码如下:
- //JWT中荷载存储内容的实体类
- @Data
- public class UserDetails {
- private String userId;
- private String username;
- private String portrait;
- }
- //用来生成和解释Token字符串,这里采用了单例模式,相当于JwtUtils
- public class UserTokenManager {
- private final String SECRET = "happy-king";
- private static volatile UserTokenManager userTokenManager;
-
- private UserTokenManager() {
- }
-
- public static UserTokenManager getInstance() {
- if (userTokenManager == null) {
- synchronized (UserTokenManager.class) {
- if (userTokenManager == null) {
- userTokenManager = new UserTokenManager();
- }
- }
- }
- return userTokenManager;
- }
-
- @SuppressWarnings("unchecked")
- public String generateToken(UserDetails userDetails, long timeout) {
- long currentTimeMillis = System.currentTimeMillis();
- return JWT.create()
- .withJWTId(UUID.randomUUID().toString())
- .withIssuedAt(new Date(currentTimeMillis))
- .withClaim("user_id", userDetails.getUserId())
- .withClaim("username", userDetails.getUsername())
- .withClaim("portrait", userDetails.getPortrait())
- .withExpiresAt(new Date(currentTimeMillis + timeout))
- .sign(Algorithm.HMAC256(SECRET));
- }
-
- public String generateToken(UserDetails userDetails) {
- return generateToken(userDetails, 1000 * 24 * 60 * 60L);
- }
-
- public UserDetails parseToken(String token) {
- try {
- DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
- Date issuedAt = decodedJWT.getIssuedAt();
- if (issuedAt.before(new Date())) {
- String userId = decodedJWT.getClaim("user_id").asString();
- String username = decodedJWT.getClaim("username").asString();
- String portrait = decodedJWT.getClaim("portrait").asString();
- UserDetails userDetails = new UserDetails();
- userDetails.setUsername(username);
- userDetails.setUserId(userId);
- userDetails.setPortrait(portrait);
- return userDetails;
- } else {
- return null;
- }
- } catch (Exception e) {
- return null;
- }
- }
- }
- //自定义的Authentication类
- public class JwtAuthenticationToken extends AbstractAuthenticationToken {
- private final UserDetails userDetails;
- private final UserTokenManager userTokenManager = UserTokenManager.getInstance();
-
- public JwtAuthenticationToken(UserDetails userDetails,
- Collection<? extends GrantedAuthority> authorities) {
- super(authorities);
- this.userDetails = userDetails;
- }
-
- @Override
- public Object getCredentials() {
- return null;
- }
-
- @Override
- public Object getPrincipal() {
- return userDetails;
- }
-
- public String generateJWT(){
- return userTokenManager.generateToken(userDetails);
- }
- }
- //登录认证主要逻辑类
- public class AuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
- @Override
- public Authentication authenticate(Authentication authentication) throws AuthenticationException {
- UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
- //获取账号密码
- String username = (String) token.getPrincipal();
- String password = (String) token.getCredentials();
- //省略了查数据库以及对比流程
-
- UserDetails userDetails = new UserDetails();
- userDetails.setUsername(username);
- userDetails.setUserId(UUID.randomUUID().toString());
- //在微服务中,角色和权限信息要保存在Redis中
- SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
- JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
- //必须设置为认证成功
- jwtAuthenticationToken.setAuthenticated(true);
- return jwtAuthenticationToken;
- }
-
- @Override
- public boolean supports(Class<?> authentication) {
- return authentication == JwtAuthenticationToken.class || authentication == UsernamePasswordAuthenticationToken.class;
- }
- }
- //然后登录成功直接写token响应到前端即可
- public class LoginSuccessHandler implements AuthenticationSuccessHandler {
- private final ObjectMapper objectMapper;
- public LoginSuccessHandler(ObjectMapper objectMapper){
- this.objectMapper = objectMapper;
- }
-
- @Override
- public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
- JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
- response.setContentType("application/json;charset=utf-8");
- HashMap<String, Object> map = new HashMap<>();
- map.put("token", jwtAuthenticationToken.generateJWT());
- String result = objectMapper.writeValueAsString(map);
- response.getWriter().write(result);
- }
- }
需要注意必须执行以下代码:
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority)); jwtAuthenticationToken.setAuthenticated(true);
因为除了认证接口外,其他所有需要授权()的接口都会被AbstractSecurityInterceptor拦截,里面有一个authenticateIfRequired方法,如果没有调用
jwtAuthenticationToken.setAuthenticated(true);则会重新进行用户认证。源码如下:
- private Authentication authenticateIfRequired() {
- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
- if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
- if (this.logger.isTraceEnabled()) {
- this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
- }
- return authentication;
- }
- authentication = this.authenticationManager.authenticate(authentication);
- // Don't authenticated.setAuthentication(true) because each provider does that
- if (this.logger.isDebugEnabled()) {
- this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
- }
- SecurityContextHolder.getContext().setAuthentication(authentication);
- return authentication;
- }
第二个问题则需要用到SpringSecurity中的拦截器,我们需要自定义一个JwtFilter,并且将其加入到UsernamePasswordAuthenticationFilter之后,代码如下:
- public final class JwtFilter extends GenericFilterBean {
- private final static String BEARER = "bearer";
-
- private final UserTokenManager userTokenManager = UserTokenManager.getInstance();
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
- }
-
- /**
- *以下抛出的异常你可以改为写出JSON响应
- **/
- private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
- if (authentication != null) {
- chain.doFilter(request, response);
- return;
- }
- String bearer = request.getHeader(BEARER);
- if (bearer == null || bearer.isEmpty()){
- bearer = request.getParameter(BEARER);
- }
- if (bearer == null || bearer.isEmpty()){
- //没有登录
- throw new AuthenticationCredentialsNotFoundException("没有用户凭证");
- }else{
- UserDetails userDetails = userTokenManager.parseToken(bearer);
- if (userDetails == null){
- throw new CredentialsExpiredException("用户凭证失效");
- }
- //微服务项目角色权限信息需要重Redis中获取,这里只是模拟
- SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
- JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
- jwtAuthenticationToken.setAuthenticated(true);
- //注意要将解释出来的JwtAuthenticationToken信息保存到SecurityContext中
- SecurityContextHolder
- .getContext()
- .setAuthentication(jwtAuthenticationToken);
- }
- chain.doFilter(request, response);
- }
- }
至此单体项目整合SpringSecurit 和JWT登录已经完成,用户登录成功后,SecurityContextPersistenceFilter中的SecurityContextRepository(默认实现是HttpSessionSecurityContextRepository)会将JwtAuthenticationToken写到session中。但是因为JWT用于分布式系统或者微服务的,所以我们不能用Session来管理,只需要修改默认的HttpSessionSecurityContextRepository为NullSecurityContextRepository并关闭session管理即可。
代码如下:
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- //移除默认登录页面
- http.removeConfigurer(DefaultLoginPageConfigurer.class);
- //关闭csrf
- http.csrf().disable()
- //关闭请求缓存
- .requestCache().disable()
- //关闭session管理
- .sessionManagement().disable()
- //修改SecurityContextRepository
- .securityContext().securityContextRepository(new NullSecurityContextRepository()).and()
- .addFilterAfter(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
-
- .authorizeRequests().anyRequest().permitAll().and()
- .formLogin()
- .loginPage("/login")
- .failureHandler(new LoginFailureHandler(objectMapper))
- .successHandler(new LoginSuccessHandler(objectMapper))
- .permitAll()
- .and()
- .httpBasic();
- }
至此,整合工作已经完毕,下面提供了代码下载地址,以上流程和WebSecurityConfigurerAdapter的实现配置用于用户微服务,然后在不同的服务下定义不同的WebSecurityConfigurerAdapter然后设置formLogin().disable()即可
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。