SpringBoot整合Spring Security+JWT实现用户注册登录

最近在做一个自己的项目,前后端分离的项目,于是整合了一下Spring
Security和JWT来实现后端系统的用户登录,自己以前没有使用过Spring Security所以这次踩坑之后记录下来。

该文较长,请耐心阅读,需要整合这部分的可以给到你一些帮助。

一、整合JWT

1.1 pom包
        <!-- jwt依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
1.2 设定jwt的配置信息

首先在配置文件中配置jwt需要的密钥、过期时间和头部信息

#配置token生成策略
config:
  jwt:
    secret: test1@Login(Auth}*^31)&heiMa% #这里可以自己定制
    expire: 30 # 过期时间,单位分钟
    header: testProject #随意定制

然后创建JwtProperties来接收配置文件中的jwt配置信息

@Configuration
@Data
@ConfigurationProperties(prefix = "config.jwt")
public class JwtProperties {

    /**
     *密钥
     */
    public  String secret;
    /**
     * 过期时间
     */
    public int expire;
    /**
     * 名称
     */
    public String header;


}
1.3 创建SecurityUser

新创建的SecurityUser继承UserDetails,另外重写UserDetails时要将return false改为return true

@Data
public class SecurityUser implements UserDetails {
    private String username;

    private String password;

    private Collection<? extends GrantedAuthority> authorities;

    public SecurityUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
1.4 创建jwt配置类JwtUtils

该配置类中指定了token生成策略

@Configuration
public class JwtUtils {

    @Resource
    private JwtProperties jwtProperties;

    /**
     * 生成token
     * @param subject
     * @return
     */
    public String createToken (String subject){
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime() + jwtProperties.expire * 1000);
        return Jwts.builder()
                .setHeaderParam("type", "JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.secret)
                .compact();
    }



    /**
     * 获取token中注册信息
     * @param token
     * @return
     */
    public Claims getTokenClaim (String token) {
        try {
            return Jwts.parser().setSigningKey(jwtProperties.secret).parseClaimsJws(token).getBody();
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }
    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        SecurityUser user = (SecurityUser) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }
    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(jwtProperties.secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }
    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 验证token是否过期失效
     * @param expirationTime
     * @return
     */
    public boolean isTokenExpired (Date expirationTime) {
        return expirationTime.before(new Date());
    }

    /**
     * 获取token失效时间
     * @param token
     * @return
     */
    public Date getExpirationDateFromToken(String token) {
        return getTokenClaim(token).getExpiration();
    }
    /**
     * 获取用户名从token中
     */
    public String getUsernameFromToken(String token) {
        return getTokenClaim(token).getSubject();
    }
    /**
     * 获取jwt发布时间
     */
    public Date getIssuedAtDateFromToken(String token) {
        return getTokenClaim(token).getIssuedAt();
    }
}

以上就是整合JWT的操作,到这里也只是做好了token的生成策略,还没有实现注册登录,实现注册登录需要Spring Security。

二、整合Spring Security

2.1 跨域配置CorsConfig

我这里首先要配置拦截器InterceptorJWT继承HandlerInterceptorAdapter
我这里通过Redis实现了单点登录,所以调用了RedisUtils。

@Component
public class RedisUtils {

	public static final String ACCESS_TOKEN = "TOKEN_GIFT_";//客户端请求服务器时携带的token参数
	public static final String REFRESH_TOKEN = "REFRESH_GIFT_";//客户端用户刷新token的参数
	public static final String PHONE_VALID_CODE = "PHONE_VALID_CODE_";//客户端短信验证码




	@Resource
	private RedisTemplate<String, String> redisTemplate;

	/**
	 * @Description: 设置缓存,k-v形式
	 * @param key
	 * @param value
	 * @param timeout:单位:毫秒
	 * @author: 
	 * @date: 
	 */
	public void setValue(String key, String value, long timeout ){
		redisTemplate.opsForValue().set(key, value, timeout , TimeUnit.MILLISECONDS);
	}

	/**
	 * @Description: 设置access_token的缓存
	 * @param userId
	 * @param value
	 * @param timeout
	 * @author: 
	 * @date:
	 */
	public void setToken(Long userId, String value, long timeout){
		setValue(ACCESS_TOKEN + userId, value, timeout);
	}
	/**
	 * @Description: 获取缓存,通过key获取value值
	 * @param key
	 * @return
	 * @author: 
	 * @date: 
	 */
	public String getValue(String key){
		return redisTemplate.opsForValue().get(key);
	}
	/**
	 * @Description: 获取redis中的access_token信息
	 * @param userId
	 * @return
	 * @author: 
	 * @date: 
	 */
	public String getToken(Long userId){
		return getValue(ACCESS_TOKEN + userId);
	}
	
	/**
	 * @Description: 删除缓存
	 * @param key
	 * @author: 
	 * @date: 
	 */
	public void delete(String key){
		redisTemplate.delete(key);
	}

}

/**
 * 请求api服务器时,对accessToken进行拦截判断,有效则可以反问接口,否则返回错误
 *
 * @author 
 */
@Component
public class InterceptorJWT extends HandlerInterceptorAdapter {

    @Autowired
    private RedisUtils redisUtils;
    @Resource
    private JwtUtils jwtUtils;

    @Resource
    private JwtProperties jwtProperties;
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) {
        // 若目标方法忽略了安全性检查,则直接调用目标方法
        if (handler.getClass().isAssignableFrom(HandlerMethod.class)) {
            //如果方法上有@IgnoreSecurity注解,则不需要进行token验证
            IgnoreSecurity ignoreSecurity = ((HandlerMethod) handler).getMethodAnnotation(IgnoreSecurity.class);
            if (ignoreSecurity != null) {
                return true;
            }
        }else {
            String token = request.getParameter(jwtProperties.header);
            if (StringUtils.isNotEmpty(token)) {
                Claims claims = jwtUtils.getTokenClaim(token);
                Long userId = (Long) claims.get("userId");
                String redisToken = redisUtils.getToken(userId);
                token = request.getParameter(jwtProperties.header);
                if (!redisToken.equals(token)) {
                    throw new SignatureException(jwtProperties.header + "失效,请重新登录。");
                }
                if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
                    throw new SignatureException(jwtProperties.header + "失效,请重新登录。");
                }
                /** 设置 identityId 用户身份ID */
                request.setAttribute("identityId", claims.getSubject());
            }else {
                throw new SignatureException(jwtProperties.header + "失效,请重新登录。");
            }
        }
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {

        super.postHandle(request, response, handler, modelAndView);
    }


    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response, Object handler, Exception ex)
            throws Exception {

        super.afterCompletion(request, response, handler, ex);
    }


    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request,
                                               HttpServletResponse response, Object handler) throws Exception {

        super.afterConcurrentHandlingStarted(request, response, handler);
    }
}

然后创建跨域配置文件CorsConfig实现WebMvcConfigurer类

@Configuration
@EnableWebMvc
public class CorsConfig implements WebMvcConfigurer {


    @Autowired
    private InterceptorJWT interceptorJWT;


    private CorsConfiguration config() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        //设置你要允许的网站域名,如果全允许则设为 *
        corsConfiguration.addAllowedOrigin("*");
        //如果要限制 HEADER 或 METHOD 请自行更改
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration( "/**", config() );
        return new CorsFilter(source);
    }




    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(interceptorJWT)
                .excludePathPatterns("/api/**")
                .excludePathPatterns("/login/**")
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");;
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

2.2 创建UserDetailsServiceImpl

创建UserDetailsServiceImpl实现UserDetailsService,其中的逻辑自己去设定实现

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService userService;
    @Autowired
    private SysUserRoleService sysUserRoleService;
    @Autowired
    private SysRoleService sysRoleService;





    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // 从数据库中取出用户信息
        SysUser user = userService.findByUserName(username);
        // 判断用户是否存在
        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 添加权限
        List<SysUserRole> userRoles = sysUserRoleService.listByUserId(user.getId());
        for (SysUserRole userRole : userRoles) {
            SysRole role = sysRoleService.get(userRole.getRoleId());
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        // 返回UserDetails实现类
        return new SecurityUser(user.getUsername(), user.getPassword(), authorities);
    }
}

2.3 创建验证登录过滤器AuthenticationFilter

创建AuthenticationFilter继承UsernamePasswordAuthenticationFilter

/**
 * 验证用户名密码正确后,生成一个token,并将token返回给客户端
 * 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 ,
 * attemptAuthentication:接收并解析用户凭证。
 * successfulAuthentication:用户成功登录后,这个方法会被调用,我们在这个方法里生成token并返回。
 * @author admin
 */
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Resource
    private JwtUtils jwtUtils;


    private AuthenticationManager authenticationManager;

    public AuthenticationFilter(AuthenticationManager authenticationManager,JwtUtils jwtUtils) {
        this.authenticationManager = authenticationManager;
        this.jwtUtils = jwtUtils;
        super.setFilterProcessesUrl("/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        // 从输入流中获取到登录的信息
        try {
            SysUser loginUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    // 成功验证后调用的方法
    // 如果验证成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult){

        SecurityUser jwtUser = (SecurityUser) authResult.getPrincipal();
        System.out.println("jwtUser:" + jwtUser.toString());
        String token = jwtUtils.createToken(jwtUser.getUsername());
        // 返回创建成功的token
        // 但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的时候应该是 `Bearer token`
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setHeader("token",token);
        ResponseUtils.out(response,Result.ok("登录成功",token));
    }



    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.getWriter().write("authentication failed, reason: " + failed.getMessage());
    }
}
创建权限过滤器AuthentizationFilter

创建AuthentizationFilter继承BasicAuthenticationFilter

public class AuthentizationFilter extends BasicAuthenticationFilter {

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private JwtUtils jwtTokenUtil;
    @Resource
    private JwtProperties jwtProperties;




    public AuthentizationFilter(AuthenticationManager authenticationManager,JwtUtils jwtUtils,JwtProperties jwtProperties) {
        super(authenticationManager);
        this.jwtTokenUtil = jwtUtils;
        this.jwtProperties = jwtProperties;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String token = request.getHeader(jwtProperties.getHeader());
        if (!StringUtils.isEmpty(token)) {
            String username = jwtTokenUtil.getUsernameFromToken(token);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(token, userDetails)){
                    // 将用户信息存入 authentication,方便后续校验
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    // 将 authentication 存入 ThreadLocal,方便后续获取用户信息
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

2.5 最后创建Spring Security配置类SecurityConfig

创建SecurityConfig继承WebSecurityConfigurerAdapter

/**
 * Security 核心配置类 
 *
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private JwtUtils jwtUtils;
    @Resource
    private JwtProperties jwtProperties;



    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }




    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                //跨域预检请求
                .antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
                //登录URL
                .antMatchers("/swagger**/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/v2/**").permitAll()
                //其它身份需要认证
                .anyRequest().permitAll()
                .and()
                .addFilter(new AuthenticationFilter(authenticationManager(),jwtUtils))
                .addFilter(new AuthentizationFilter(authenticationManager(),jwtUtils,jwtProperties))
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //退出登录器
        http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
    }


    @Bean
    @Override
    public AuthenticationManager authenticationManager()throws Exception{
        return super.authenticationManager();
    }


}


三、测试

到这里就完成了JWT+SpringSecurity的配置,接下来就要实现注册登录。

@RestController
@Api(value = "LoginController", tags = {"LoginController"}, description = "登录")
public class LoginController extends BaseController {


    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;





    @PostMapping("/register")
    public String registerUser(String name,String password){
        SysUser user = new SysUser();
        user.setUsername(name);
        user.setPassword(bCryptPasswordEncoder.encode(password));
        sysUserService.insert(user);
        return "success";
    }
 }

以上的代码时注册的逻辑,因为Spring Security自带了登录接口,可以在AuthenticationFilter中配置
SpringBoot整合Spring Security+JWT实现用户注册登录_第1张图片

另外我的配置文件中配置了请求路径的设定,所以我的登录路径为localhost:8001/api/login,注册接口为localhost:8001/api/register
SpringBoot整合Spring Security+JWT实现用户注册登录_第2张图片

效果

登录效果如下:
SpringBoot整合Spring Security+JWT实现用户注册登录_第3张图片

SpringBoot整合Spring Security+JWT实现用户注册登录_第4张图片

如果需要该例子的话可以私聊我,我暂时还没放到GitHub,后续发布到GitHub会放到该处。

你可能感兴趣的:(《知识增强系列》,Java,jwt,spring,java,spring,security)