最近在做一个自己的项目,前后端分离的项目,于是整合了一下Spring
Security和JWT来实现后端系统的用户登录,自己以前没有使用过Spring Security所以这次踩坑之后记录下来。
该文较长,请耐心阅读,需要整合这部分的可以给到你一些帮助。
<!-- 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>
首先在配置文件中配置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;
}
新创建的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;
}
}
该配置类中指定了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。
我这里首先要配置拦截器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/");
}
}
创建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);
}
}
创建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继承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);
}
}
创建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中配置
另外我的配置文件中配置了请求路径的设定,所以我的登录路径为localhost:8001/api/login
,注册接口为localhost:8001/api/register
如果需要该例子的话可以私聊我,我暂时还没放到GitHub,后续发布到GitHub会放到该处。