SpringSecurity是一个非常强大的安全验证框架,它通过一个过滤器链完成了对资源的访问控制。security框架本身提供了多种验证方式,如OA,Session等,但是有些情况下还是无法满足用户的需求,故此security框架也有非常大的扩展性,它允许用户自定义认证流程,下面我们就结合security实现一个jwt认证流程。
security框架中通过UsernamePasswordAuthenticationFilter来拦截登录请求,并将登录信息封装为待验证的token对象。由于我们需要自定义认证流程,所以要继承UsernamePasswordAuthenticationFilter的父类实现自己的拦截器来构造token。
public class JwtAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationProcessingFilter() {
//该过滤器会拦截哪些路径和类型的请求
super(new AntPathRequestMatcher("/Jwtlogin", "POST"));
}
/**
* 该方法是实现将用户登录信息从request提取,并封装成一个未认证的token传给AuthenticationManager处理
*
* @param httpServletRequest
* @param httpServletResponse
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
try {
String username=httpServletRequest.getParameter("username");
String password=httpServletRequest.getParameter("password");
JwtToken jwtToken=new JwtToken(username,password);
//将请求的会话id,IP地址保存在token中
jwtToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
//调用认证管理器对token进行认证
Authentication authentication=this.getAuthenticationManager().authenticate(jwtToken);
return authentication;
}catch (Exception e)
{
throw new BadCredentialsException("校验凭证失败,"+e.getMessage());
}
}
}
由于JwtAuthenticationProcessingFilter会调用AuthenticationManager进行认证,而AuthenticationManager内部存在多个认证方式,它通过token的类型来选择其中一种进行验证,因此我们需要实现一个自己的token。
public class JwtToken extends AbstractAuthenticationToken {
private Object Principal;
private Object Credential;
/**
* 构造等待认证的token
* @param principal
* @param credential
*/
public JwtToken(Object principal, Object credential) {
super(null);
Principal = principal;
Credential = credential;
//设置token状态为未认证
setAuthenticated(false);
}
/**
* 构造已认证的token
* @param authorities
* @param principal
* @param credential
*/
public JwtToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credential) {
//保存权限
super(authorities);
Principal = principal;
Credential = credential;
//设置token状态为已认证
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.Credential;
}
@Override
public Object getPrincipal() {
return this.Principal;
}
}
Security的认证方式都实现AuthenticationProvider接口,所以我们只需要这个接口就可以自定义认证方式了,这个接口有authenticate和supports两个方法,authenticate接收一个待认证的token作为参数,认证成功则重新构造一个已认证的token并返回。supports方法用于判断是否该认证方式支持哪种token。
public class JwtAuthProvider implements AuthenticationProvider{
@Resource(name = "UserDetailServiceImpl")
private UserDetailsService userDetailsService;
@Autowired
private JwtPasswordEncoder encoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtToken jwtToken= (JwtToken) authentication;
UserDetails details=userDetailsService.loadUserByUsername(jwtToken.getName());
AuthenticationCheck(details,jwtToken);
SecurityCheck(details);
JwtToken token=new JwtToken(details.getAuthorities(),details,jwtToken.getCredentials());
return token;
}
@Override
public boolean supports(Class<?> aClass) {
return JwtToken.class.isAssignableFrom(aClass);
}
public void SecurityCheck(UserDetails details){
if(!details.isAccountNonLocked()){
throw new BadCredentialsException("The Account is locked");
}
}
public void AuthenticationCheck(UserDetails details,JwtToken jwtToken){
if(jwtToken.getCredentials()==null){
throw new BadCredentialsException("The password not null");
}
if(!encoder.matches(details.getPassword(),jwtToken.getCredentials().toString())){
throw new BadCredentialsException("The Password verification failed");
}
}
}
在Security框架自己提供的认证方式中都是通过UserDetailsService接口的loadUserByUsername方法从数据库加载用户信息,因此这里我们入乡随俗实现该接口来加载用户信息
public class UserDetailServiceImpl implements UserDetailsService{
@Autowired
private UserService userServiceImp;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user=userServiceImp.getUserByAccount(s);
if(user==null)throw new UsernameNotFoundException("用户账号:"+s+"不存在");
Optional<String> op=userServiceImp.getUserAuthority(user.getUsername()).stream().findFirst();
if(op.isPresent())return new SecurityUser(user,op.get());
else return new SecurityUser(user);
}
}
public class SecurityUser implements UserDetails {
private User user;
private GrantedAuthority authorities;
public SecurityUser(User user,String role) {
this.user = user;
this.authorities= new SimpleGrantedAuthority(role);
}
public SecurityUser(User user){this.user=user;}
public SecurityUser() {
}
public SecurityUser(GrantedAuthority authoritie) {
this.authorities = authorities;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> list=new ArrayList<GrantedAuthority>();
list.add(this.authorities);
return list;
}
public void setAuthorities(String authorities) {
this.authorities=new SimpleGrantedAuthority(authorities);
}
@Override
@JSONField(serialize = false)
public String getPassword() {
return this.user.getUserpassword();
}
@Override
@JSONField(serialize = false)
public String getUsername() {
return this.user.getUseraccount();
}
@Override
@JSONField(serialize = false)
public boolean isAccountNonExpired() {
return false;
}
@Override
@JSONField(serialize = false)
public boolean isAccountNonLocked() {
return this.user.getStatus().equals("0");
}
@Override
@JSONField(serialize = false)
public boolean isCredentialsNonExpired() {
return false;
}
@Override
@JSONField(serialize = false)
public boolean isEnabled() {
return false;
}
}
拦截所有请求,获取请求头的JWT token,如没有JWT token则直接放行,因为后续还有过滤器进行拦截,AnonymousAuthenticationFilter发现SecurityContextHolder中没有待验证的token时会加入一个已验证的匿名token,最终走到FilterSecurityInterceptor时,权限管理器再根据资源访问规则决定是否允许匿名用户访问资源。获取到JWT token并解密通过则构造已认证的token加入SecurityContextHolder。
public class JwtAccessFilter extends OncePerRequestFilter {
public String getToken(HttpServletRequest httpServletRequest) {
return httpServletRequest.getHeader("Authorization");
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token=this.getToken(httpServletRequest);
if(token==null||token.isEmpty()){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
SecurityUser securityUser;
try {
String jsonUser=JwtUtil.parseToken(token).get("data");
securityUser=JSONObject.parseObject(jsonUser,SecurityUser.class);
JSONObject jsonObject=JSONObject.parseObject(jsonUser);
securityUser.setAuthorities(jsonObject.getJSONArray("authorities").getJSONObject(0).getString("authority"));
}catch (Exception e){
e.printStackTrace();
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().print(JSONObject.toJSONString(new AjaxResponseBody("401","token已失效,请重新登录",null)));
return;
}
JwtToken jwtToken=new JwtToken(securityUser.getAuthorities(),securityUser,"");
jwtToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(jwtToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
@Component("AccessRuleService")
public class AccessRuleService {
private AntPathMatcher antPathMatcher=new AntPathMatcher();
public boolean controll(HttpServletRequest request, Authentication authentication){
String url=request.getRequestURI();
if(antPathMatcher.match("/anyone",url)) return true;
if(authentication instanceof AnonymousAuthenticationToken)return false;
SecurityUser securityUser= (SecurityUser) authentication.getPrincipal();
Optional<String> op=securityUser.getAuthorities().stream().map(i->i.getAuthority()).findFirst();
if(!op.isPresent())return false;
List<String> Urls=queryUrlByAuthorities(op.get());
for (String l:Urls) {
if(antPathMatcher.match(l,url))return true;
}
return false;
}
public List<String> queryUrlByAuthorities(String authoritie){
switch (authoritie){
case "USER":{
return Arrays.asList("/user");
}
case "ADMIN":{return Arrays.asList("/user","/admin");}
case "SUPER ADMIN":{return Arrays.asList("/user","/admin","/super");}
default:return null;
}
}
}
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
try {
AjaxResponseBody ajaxResponseBody = null;
SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
httpServletResponse.setHeader("Content-Type", "application/json;charset=utf-8");
JSONObject json=new JSONObject();
JSONObject userjson= (JSONObject) JSONObject.toJSON(securityUser.getUser());
json.put("token",JwtUtil.createJwtToken(JSONObject.toJSONString(securityUser)));
userjson.remove("userpassword");
json.put("userinfo",userjson);
ajaxResponseBody = new AjaxResponseBody("200", "success", json);
httpServletResponse.getWriter().write(JSONObject.toJSONString(ajaxResponseBody));
} catch (Exception e) {
e.printStackTrace();
}
}
}
由于token的无状态特点,因此我们很难自行失效token。笔者感觉非要失效token的话,在网上找到的诸多方法中,黑名单机制或许是一种比较好的方式。将需要失效的token存入数据库,每次访问时查看token存在失效表中。这里就由读者自行实现吧,笔者懒得弄了。
@SpringBootConfiguration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFailHandler myAuthenticationFailHandler;
@Autowired
private JwtAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private JwtLogoutSuccessHandler myLogoutSuccessHandler;
@Autowired
private JwtLogoutHandler myLogoutHandler;
@Autowired
private JwtAccessFilter jwtAccessFilter;
@Autowired
private JwtAuthProvider provider;
@Bean(name = "UserDetailServiceImpl")
public UserDetailsService customUserService() {
return new UserDetailServiceImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义UserDetailsService
auth.userDetailsService(customUserService()).passwordEncoder(new JwtPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthenticationProcessingFilter processingFilter=new JwtAuthenticationProcessingFilter();
//设置认证失败后的处理器
processingFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
//设置认证成功的处理器
processingFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
//设置用于认证的认证管理器
processingFilter.setAuthenticationManager(this.authenticationManagerBean());
//将认证流程过滤器放到security的过滤器链中
http.addFilterAfter(processingFilter, UsernamePasswordAuthenticationFilter.class)
//将自定义的认证方式加入过认证管理器
.authenticationProvider(provider)
//token拦截器加入过滤器链中
.addFilterAfter(jwtAccessFilter,JwtAuthenticationProcessingFilter.class)
.exceptionHandling()
//设置访问权限不足时的处理器访问
.accessDeniedHandler(new JwtAccessDeniedHandler())
//设置匿名用户访问被拒绝时的处理器
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().csrf().disable();
//配置登出处理器
http.logout().addLogoutHandler(myLogoutHandler).logoutSuccessHandler(myLogoutSuccessHandler);
//配置资源访问规则
http.authorizeRequests().anyRequest().access("@AccessRuleService.controll(request,authentication)");
}
}