
实现代码以【spring-security基础】基于数据库的认证方式 为前提
jwt需要引入对应工具包,用于维护token
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>这部分配置了自定义异常处理及过滤器
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 未授权访问
.exceptionHandling()
// 认证失败
.authenticationEntryPoint(myUnAuthEntryPoint)
// 鉴权失败
.defaultAccessDeniedHandlerFor(new MyAccessDeniedHandler(),new AntPathRequestMatcher("/**"))
.and()
// 关闭csrf检查,此项检查开启时可能导致过滤链被提前终止
.csrf().disable()
// // 资源权限
.authorizeRequests().anyRequest().authenticated()
//关闭basic验证
.and().httpBasic().disable()
// // 登录设置 (自定义认证过滤器后失效,如本例中已经定义MyTokenLoginFilter)
.formLogin().loginProcessingUrl("/noauth/sublogin")
.and()
// // 退出登录设置
.logout().logoutUrl("/noauth/logout").addLogoutHandler(myLogoutHandler)
.and()
// // 认证处理器
.addFilter(new MyTokenLoginFilter(authenticationManager(), myTokenConfig))
// // 授权处理器
.addFilter(new MyTokenAuthFilter(authenticationManager(),myTokenConfig));
}配置忽略鉴权路径
@Override
public void configure(WebSecurity webSecurity) throws Exception {
// 忽略鉴权及认证配置,配置此项的地址将不会被security过滤连过滤(包括认证、登出等)
webSecurity.ignoring().antMatchers("/auth/**");
}配置密码处理器及用户查询服务
@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
// 设置用户查询服务及密码处理器
authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(myPasswordEncoder);
// 当手动注入AuthenticationProvider时,需要做如下声明
//authenticationManagerBuilder.authenticationProvider(daoAuthenticationProvider());
}自定义认证过滤器主要解决认证信息的获取及处理,比如当前后端分离项目中,密码或者用户名使用加密等手段传递时,需要在这里定义自己的解密方法,以供security完成认证。 实现自定义认证只需要继承AbstractAuthenticationProcessingFilter (或其子类)即可,
覆盖Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException 方法,用于处理前端请求过来的认证信息,比如以application/json 方式请求过来的数据,或加密数据,在这个方法内,需要根据将数据处理成密码处理器所能完成认证的格式(比如简单的equals)。
覆盖void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) 方法,完成认证成功是的token生成及缓存工作。(这里用本地缓存模拟redis效果)
覆盖void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) 用于处理在认证失败时的响应信息处理
package cn.com.demo.filter;
import cn.com.demo.common.vo.BaseRepVo;
import cn.com.demo.config.MyUser;
import cn.com.demo.config.MyTokenConfig;
import cn.com.demo.entity.UserInfo;
import cn.com.demo.util.LocalCache;
import cn.com.demo.util.ResponseUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
public class MyTokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
MyTokenConfig myTokenConfig;
public MyTokenLoginFilter(AuthenticationManager authenticationManager, MyTokenConfig myTokenConfig) {
this.authenticationManager = authenticationManager;
this.myTokenConfig = myTokenConfig;
// 设置登录地址及请求方式 (默认:/login POST)
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/noauth/sublogin", "POST"));
}
// 获取登录信息
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
// 修改获取用户名密码的过程 ,对于前后端分离项目中密码加密存储时,在这一步进行密码解密 以保证密码能正确处理密码信息
UserInfo userInfo = new ObjectMapper().readValue(request.getInputStream(), UserInfo.class);
// 提交security处理认证过程
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userInfo.getUsername(), userInfo.getPassword(), new ArrayList<GrantedAuthority>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("获取认证信息异常");
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
System.out.println("登录成功");
// 获取认证成功信息
MyUser user = (MyUser) authResult.getPrincipal();
// 缓存权限信息
LocalCache.put(user.getUsername(),user.getAuthorities());
// 生成token
String token = myTokenConfig.getToken(user.getUsername());
HashMap<String,String> data = new HashMap<>();
data.put("token",token);
ResponseUtil.out(response,new BaseRepVo(data));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
System.out.println("登录失败");
ResponseUtil.error(response,failed.getMessage());
}
}自定义用户服务,用于根据用户名从数据库(或其他鉴权接口)中查询用户及权限信息,以用于security的鉴权。默认情况下UsernameNotFoundException异常会被转换成BadCredentialsException,如需处理,需要手动配置authenticationManagerBuilder.authenticationProvider(provider)进行覆盖(demo代码中已实现)
package cn.com.demo.service;
import cn.com.demo.config.MyUser;
import cn.com.demo.dao.RolePermissionDao;
import cn.com.demo.dao.UserInfoDao;
import cn.com.demo.entity.UserInfo;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserInfoDao userInfoDao;
@Autowired
private RolePermissionDao rolePermissionDao;
/**
* 按需决定是否需要引入密码加密
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
*/
@Override
public UserDetails loadUserByUsername(String usename) throws UsernameNotFoundException {
// 查询数据库
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",usename);
UserInfo userInfo = userInfoDao.selectOne(queryWrapper);
if (userInfo ==null) {
throw new UsernameNotFoundException("用户不存在");
}
// 获取用户角色
List<String> roles = rolePermissionDao.getRuleByUserId(userInfo.getId());
// 获取用户权限(url)
List<String> per = rolePermissionDao.getPermissByUserId(userInfo.getId());
for (String role : roles) {
per.add("ROLE_"+role);
}
List<GrantedAuthority> auths = new ArrayList<>();
for (String s : per) {
GrantedAuthority e = new SimpleGrantedAuthority(s);
auths.add(e);
}
// 返回认证主体UserDetails
return new MyUser(userInfo.getUsername(),userInfo.getPassword(),auths);
//使用非自定义的密码,可能需要进行加密后再放置进认证主体密码属性中去
// return new User(userInfo.getUsername(),bCryptPasswordEncoder.encode(userInfo.getPassword()),auths);
}
}自定义密码处理器用于密码加密(如存储到数据库前的加密)及匹配鉴权
package cn.com.demo.config;
import cn.com.demo.util.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 自定义密码加密方式
*/
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/**
* 加密密码
* @param rawPassword 原始密码
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
/**
* 密码解密
* @param rawPassword 原始密码(页面提交过来的)
* @param encodedPassword 已存储的密码(数据库等)
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equalsIgnoreCase(MD5.encrypt(rawPassword.toString()));
}
}自定义鉴权主要收集用户的权限,提交security进行鉴权。 jwt实现时,通过访问request的header部分查询token,并进行有效期检查及获取其权限,这部分自定义可以实现jwt token有效期的检查,同时也可以完成手动退出时token仍在有效期可继续使用的问题。
package cn.com.demo.filter;
import cn.com.demo.config.MyTokenConfig;
import cn.com.demo.util.LocalCache;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class MyTokenAuthFilter extends BasicAuthenticationFilter {
MyTokenConfig myTokenConfig;
public MyTokenAuthFilter(AuthenticationManager authenticationManager, MyTokenConfig myTokenConfig) {
super(authenticationManager);
this.myTokenConfig = myTokenConfig;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取认证成功的用户授权信息
String token = request.getHeader(myTokenConfig.getTokenConfigProperties().getHeadName());
if(!StringUtils.isEmpty(token)) {
String userName = myTokenConfig.getUserName(token);
List<GrantedAuthority> authorities = (List<GrantedAuthority>) LocalCache.get(userName);
if(authorities != null) {
// 放置权限到security上下文中
UsernamePasswordAuthenticationToken auths = new UsernamePasswordAuthenticationToken(userName, token, authorities);
SecurityContextHolder.getContext().setAuthentication(auths);
}
}
// 过滤链需要继续向下
chain.doFilter(request,response);
}
@Override
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {
System.out.println("鉴权通过。。。。");
}
@Override
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
System.out.println("鉴权失败");
}
}需要注意的是,虽然覆写了onSuccessfulAuthentication、onUnsuccessfulAuthentication但并没有在执行链(doFilter\doFilterInternal)中完整处理,如需处理可参考其父类进行处理(因为这两个方法为BasicAuthenticationFilter 增强的两个方法,而非标准处理链上的方法)。
这部分处理包括两部分;认证过程中异常的处理及权限不足时的处理,处理这部分主要用于在用户没有权限时覆盖默认的page转发及HTTP状态码
public class MyUnAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, BaseRepVo.UNAUTH);
}
}public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseUtil.out(response, BaseRepVo.FORBIDDEN);
}
}通过重写configure(WebSecurity webSecurity) 方法,配置其ignoring().antMatchers(String… antPatterns)方法 配置忽略过滤的资源,需要注意的是这里配置的资源将不会完整执行security的过滤器链,比如如果将认证过滤器(自定义)的url配置到这里,你的自定义认证过滤器也就不会生效了。 具体代码见整体配置。
通过实现LogoutHandler 接口完成退出登录的清理工作,需要注意的是,security有自己的会话维护(实现了SecurityContextHolderStrategy接口)。
public class MyLogoutHandler implements LogoutHandler {
@Autowired
private MyTokenConfig myTokenConfig;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader(myTokenConfig.getTokenConfigProperties().getHeadName());
if(!StringUtils.isEmpty(token)) {
// token清理工作,如在redis中删除token,防止未过期的token继续使用
String userName = myTokenConfig.getUserName(token);
LocalCache.remove(userName);
}
ResponseUtil.ok(response);
}
}token的处理主要包括基于用户名(用户信息)生成token,及对token的解析(这里仅在token中放置了用户名信息)
package cn.com.demo.config;
import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.compression.GzipCompressionCodec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyTokenConfig {
@Autowired
TokenConfigProperties tokenConfigProperties;
public String getToken(String userName) {
return Jwts.builder()
.setSubject(userName)
.setExpiration(new Date(System.currentTimeMillis() + tokenConfigProperties.getExpireTime()*1000))
.signWith(SignatureAlgorithm.HS512,tokenConfigProperties.getSignKey())
.compressWith(new GzipCompressionCodec()).compact();
}
public String getUserName(String token) {
try {
return Jwts.parser().setSigningKey(tokenConfigProperties.getSignKey()).parseClaimsJws(token).getBody().getSubject();
} catch (Exception e) {
e.printStackTrace();
System.out.println("token 解析异常");
}
return null;
}
public TokenConfigProperties getTokenConfigProperties(){
return tokenConfigProperties;
}
}demo代码地址 demo