前言
Spring Security默认采用用户名和密码认证方式,前后端分离项目中基于token进行认证,本文将讲解Spring Boot集成Spring Security实现jwt认证。
集成步骤
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
JWT工具类
public static String createJWT(String subject, String issue, Object claim,
long ttlMillis)
{
long nowMillis = System.currentTimeMillis();
long expireMillis = nowMillis + ttlMillis;
String result = Jwts.builder().setSubject(subject).setIssuer(issue)
.setExpiration(new Date(expireMillis)).claim("user", claim).setId(issue)
.signWith(getSignatureAlgorithm(), getSignedKey())
.compressWith(CompressionCodecs.DEFLATE).compact();
return result;
}
public static String getUsernameFromToken(String token)
{
Claims claims =getClaims(token);
if(claims==null)
{
return "";
}
String username= claims.getSubject();
return username;
}
private static Key getSignedKey()
{
byte[] apiKeySecretBytes = DatatypeConverter
.parseBase64Binary(getAuthKey());
Key signingKey = new SecretKeySpec(apiKeySecretBytes,
getSignatureAlgorithm().getJcaName());
return signingKey;
}
private static SignatureAlgorithm getSignatureAlgorithm()
{
return SignatureAlgorithm.HS256;
}
public static String getAuthKey()
{
String auth = JWT_SCRET_KEY;
return auth;
}
JWT认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter
{
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException
{
String uri=request.getRequestURI();
//过滤无需认证请求
// 从 HTTP 请求中获取 token
String token = this.getTokenFromHttpRequest(request);
logger.info("request token :{}",token);
//传入token为空
if(StringUtils.isEmpty(token))
{
logger.error("token is null");
filterChain.doFilter(request,response);
return;
}
Authentication authentication = getAuthentication(token,request);
//token验证失败
if(StringUtils.isEmpty(authentication))
{
logger.error("authentication is null");
filterChain.doFilter(request,response);
return;
}
// 将认证信息存入 上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
/**
* 验证token
* @param token
* @return
*/
private Authentication getAuthentication(String token,HttpServletRequest request)
{
//从token中获取用户名
String userName=JwtUtil.getUsernameFromToken(token);
logger.info("getAuthentication userName:{}",userName);
if(!StringUtils.isEmpty(userName) && SecurityContextHolder.getContext().getAuthentication() == null)
{
logger.info("getAuthentication username:{}"+userName);
AuthedUser authedUser = jwtService.loadUserByUserId(userName);
//验证token
if(JwtUtil.validateToken(token))
{
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(authedUser,null,authedUser.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return usernamePasswordAuthenticationToken;
}
}
return null;
}
/**
* 获取token信息
* @param request
* @return
*/
private String getTokenFromHttpRequest(HttpServletRequest request)
{
String token = request.getHeader("token");
if(StringUtils.isEmpty(token))
{
token=request.getParameter("token");
}
return token;
}
说明:前端传入token,经过过滤器对token进行验证,成功则将token保存到SecurityContextHolder上下文中。
JwtService业务类
@Service
@Transactional(rollbackFor=Exception.class)
public class JwtServiceImpl implements JwtService
{
@Autowired
private LoginService userService;
public AuthedUser loadUserByUserId(String userId)
throws UsernameNotFoundException
{
AuthedUser authedUser=userService.loadUserByUsername(userId);
if(authedUser==null)
{
throw new UsernameNotFoundException("用户名或密码错误");
}
return authedUser;
}
}
登录业务类
public AuthedUser loadUserByUsername(String userId)
throws UsernameNotFoundException
{
User user= userMapper.getUserByUserId(userId);
if(user==null)
{
throw new RuntimeException("用户名或密码不正确");
}
//加载角色
List<Role> roles= userMapper.getRolesByUserOid(user.getOid());
if(CollectionUtils.isEmpty(roles))
{
throw new RuntimeException("用户角色为空");
}
Set<Integer> roleOids =new HashSet<>();
for(Role role:roles)
{
roleOids.add(role.getOid());
}
//加载权限
List<Func> funcs=userMapper.getResByRoleOid(roleOids);
AuthedUser authedUser =new AuthedUser();
authedUser.setUserName(userId);
authedUser.setPassword(user.getPwd());
//设置角色
Set<Role> roleSet=new HashSet<>(roles);
authedUser.setRoles(roleSet);
//设置权限
Set<Func> funcSet=new HashSet<>(funcs);
authedUser.setResources(funcSet);
return authedUser;
}
loginService 接口继承UserDetailsService接口,重写loadUserByUsername方法。
认证实体类
public class AuthedUser implements UserDetails ,Serializable
{
private static final long serialVersionUID = 600774151302261558L;
private String userName;
private String password;
/**
* 角色列表
*/
private Set<Role> roles = new HashSet<Role>();
/**
* 权限列表
*/
private Set<Func> resources = new HashSet<Func>();
}
修改Spring Security 配置
/**
* DSL编程
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login","/index","/login-error","/css/**","/js/**").permitAll()
//任何请求需要认证
.anyRequest().authenticated()
// 表单认证、登录页面
.and().formLogin().loginPage("/login")
.failureUrl("/login-error").defaultSuccessUrl("/index").
successHandler(customLoginSuccessHandler())
//登出
.and().logout().logoutUrl("/logout")
//关闭跨域访问
.and().csrf().disable()
//未授权页面访问
.exceptionHandling().accessDeniedPage( "/403" );
//添加Token认证过滤器
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
//未登录验证、鉴权异常
http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeineHandler());
//使用无状态session,session不会储存用户状态
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception
{
return new JwtAuthenticationFilter();
}
说明:配置类中添加jwt认证过滤器。
测试
用户登录成功
访问/app/sys/user/list接口,header中未携带token
访问/app/sys/user/list接口,携带正常token
总结
Spring Security 虽然比较重量级,但是提供了非常多的扩展性,可以实现多种方式认证,本文讲解了JWT认证实现,如有问题,可以随时反馈。