原始认证流程通常会配合Session一起使用,但前后端分离后就用不到Session了
SpringSecurity默认的认证流程如下图(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
2、前后端分离认证流程DaoAuthenticationProvider继承AbstractUserDetailsAuthenticationProvider抽象类,而AbstractUserDetailsAuthenticationProvider抽象类又实现了AuthenticationProvider这个接口。
AuthenticationProvider接口和AuthenticationManager接口都有 authenticate() 这个方法
认证流程:
1、传入用户名和密码
2、UsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象
3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写的authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法
6、loadUserByUsername()方法会返回UserDetails对象,认证成功逐一返回上一层
前后端分离后,我们要求在认证成功或者失败的时候能够返回对应的状态码,这时我们不再使用Session进行认证管理,而常采用jwt(JSON Web Token)的方式进行认证,这里引出两种前后端分离的写法
(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
2.1、继承UsernamePasswordAuthenticationFilter的写法无论使用下面哪一种写法,这里都需要在UsernamePasswordAuthenticationFilter前面添加一个过滤器,用于进行Token认证,如果Token认证成功,则表示该用户已登录;Token认证失败则表明未登录或者登陆已过期。
2.2、自定义写法认证流程:
1、传入用户名和密码
2、MyUsernamePasswordAuthenticationFilter会把用户名和密码封装成Authentication对象
3、然后又再调用AuthenticationManager接口中的authenticate()方法进行认证,在AuthenticationManager接口的实现类ProviderManager中又调用了重写的authenticate()方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法
6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象
7、在继承WebSecurityConfigurerAdapter的类中设置登陆成功、失败处理器,处理器内部定义好返回的状态码等信息
2.3、区别UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口
认证流程:
1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层
2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象
3、然后调用AuthenticationManager的authenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法
6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象
二、数据库的设计1、使用UsernamePasswordAuthenticationFilter的写法需要使用登陆成功、失败处理器,自定义的写法不需要,自定义的写法可以自定义失败处理器(包括认证异常和授权异常,即登陆失败和没有权限)
2、使用UsernamePasswordAuthenticationFilter的写法对于扩展写法没那么友好,比如说添加手机验证码
该示例是上面自定义的前后端分离的写法
1、用户表 2、用户角色关系表 3、角色表 4、图片表 5、点赞表 三、初始配置这里使用的是Oracle数据库,这里没有权限的表,但是使用角色来判断也差不多
1、项目结构 2、导入依赖SpringBoot 版本是 2.6.0
代码生成器这里最开始使用的是mysql 8.X版本的,读者需要自己修改一下数据库的名字,如果是mysql 5.X还需要修改一下驱动
后面才改用Oracle数据库,这里的代码就懒得改了
package com.guet.APPshareimage;import com.baomidou.mybatisplus.annotation.DbType;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;import com.baomidou.mybatisplus.generator.AutoGenerator;import com.baomidou.mybatisplus.generator.config.*;import com.baomidou.mybatisplus.generator.config.rules.DateType;import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;import org.apache.commons.lang3.StringUtils;import java.util.Scanner;public class CodeGenerator { public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 创建代码生成器对象 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); gc.setOutputDir(scanner("请输入你的项目路径") + "/src/main/java"); //作者 gc.setAuthor("LZDWTL"); //生成之后是否打开资源管理器 gc.setOpen(false); //重新生成时是否覆盖文件 gc.setFileOverride(false); //%s 为占位符 //mp生成service层代码,默认接口名称第一个字母是有I gc.setServiceName("%sService"); //设置主键生成策略 自动增长 gc.setIdType(IdType.AUTO); //设置Date的类型 只使用 java.util.date 代替 gc.setDateType(DateType.ONLY_DATE); //开启实体属性 Swagger2 注解 gc.setSwagger2(true); mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/shareimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("shareimage"); dsc.setPassword("888888"); //使用mysql数据库 dsc.setDbType(DbType.MYSQL); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); //pc.setModuleName(scanner("请输入模块名")); pc.setParent("com.guet.APPshareimage"); pc.setController("controller"); pc.setService("service"); pc.setServiceImpl("service.impl"); pc.setMapper("mapper"); pc.setEntity("entity"); pc.setXml("mapper"); mpg.setPackageInfo(pc); // 策略配置 StrategyConfig strategy = new StrategyConfig(); //设置哪些表需要自动生成 strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); //实体类名称驼峰命名 strategy.setNaming(NamingStrategy.underline_to_camel); //列名名称驼峰命名 strategy.setColumnNaming(NamingStrategy.underline_to_camel); //使用简化getter和setter strategy.setEntityLombokModel(true); //设置controller的api风格 使用RestController strategy.setRestControllerStyle(true); //驼峰转连字符 strategy.setControllerMappingHyphenStyle(true); //忽略表中生成实体类的前缀 //strategy.setTablePrefix("t_"); mpg.setStrategy(strategy); mpg.execute(); }}
运行代码生成器,复制路径输入,然后依次输入数据库中表的名字
D:WorkSpaceJavaWorkSpceidealAPP-shareimageAPP-shareimaget_user,t_picture,t_like,t_user_role,t_role
4、配置application.yml根据自己的数据库和redis进行配置
server: port: 8080spring: # 数据库配置 datasource: driver-class-name: oracle.jdbc.driver.OracleDriver url: jdbc:oracle:thin:@120.77.80.135:1521:orcl username: XXXXXX password: XXXXXX # 连接池 hikari: # 连接池名 pool-name: DateHikariCP # 最小空闲连接数 minimum-idle: 5 # 空闲连接最大存活时间,默认600000(10分钟) idle-timeout: 180000 # 最大连接数,默认10 maximum-pool-size: 10 # 从连接池返回的连接自动提交 auto-commit: true # 连接最大存活时间,1800000(30分钟) max-lifetime: 1800000 # 连接超时时间,默认30000(30秒) connection-timeout: 30000 # 测试连接是否可用的查询语句 #connection-test-query: SELECt 1 #这个是mysql的测试语句 connection-test-query: SELECT * from dual #这个是oracle的测试语句 #redis配置 redis: #服务器地址 host: 120.77.80.135 #端口 port: 6379 #redis密码 password: XXXXXX #数据库,默认是0 database: 0 #超时时间 timeout: 1209600000ms lettuce: pool: #最大链接数,默认8 max-active: 8 #最大连接阻塞等待时间,默认-1 max-wait: 10000ms #最大空闲连接,默认8 max-idle: 200 #最小空闲连接,默认0 min-idle: 5mybatis-plus: mapper-locations: classpath:/mapper/*Mapper.xml type-aliases-package: com.guet.APPshareimage.entitylogging: level: com.guet.shareimage.mapper: debugjwt: # JWT存储的请求头 tokenHeader: Authorization # JWT 加解密使用的密钥 secret: lzdwtl # JWT的超期限时间(1000*60*60*24*14)14天,即两周 expiration: 1209600000 # JWT 负载中拿到开头 tokenHead: Bearerrole: roleid: 1
5、其他配置、工具类 5.1、SpringSecurity配置类@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyOncePerRequestFilter myOncePerRequestFilter; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { //1、关闭csrf,关闭Session http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //2、设置不需要认证的URL http .authorizeRequests() //允许未登录的用户进行访问 .antMatchers("/doLogin").anonymous() //其余url都要认证才能访问 .anyRequest().authenticated(); }}
5.2、JSON格式返回配置类public abstract class JSONAuthentication { protected void WriteJSON(HttpServletRequest request, HttpServletResponse response, Object obj) throws IOException, ServletException { //这里很重要,否则页面获取不到正常的JSON数据集 response.setContentType("application/json;charset=UTF-8"); //跨域设置 response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Method", "POST,GET"); //输出JSON PrintWriter out = response.getWriter(); out.write(JSON.toJSONString(obj)); out.flush(); out.close(); }}
5.3、密码编码类@Componentpublic class BCryptPasswordEncoderUtil extends BCryptPasswordEncoder { @Override public String encode(CharSequence rawPassword) { return super.encode(rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return super.matches(rawPassword,encodedPassword); }}
5.4、JWT工具类@Componentpublic class JwtUtil { private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); private static String SECRET_KEY; private static Long EXPIRATION_TIME; //对于静态变量,需要使用set方法才能使用设置好的字段值 @Value("${jwt.secret}") public void setSECRET_KEY(String SECRET_KEY) { this.SECRET_KEY = SECRET_KEY; } @Value("${jwt.expiration}") public void setEXPIRATION_TIME(Long expiration) { this.EXPIRATION_TIME = expiration; } public static String getUUID() { String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if (ttlMillis == null) { ttlMillis = EXPIRATION_TIME; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("LZDWTL") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } public static SecretKey generalKey() { byte[] encodedKey = base64.getDecoder().decode(SECRET_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); }}
5.5、Redis工具类@SuppressWarnings(value = { "unchecked", "rawtypes" })@Componentpublic class RedisCache{ @Autowired public RedisTemplate redisTemplate; public
package com.guet.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configurationpublic class RedisConfig { @Bean @SuppressWarnings(value = { "unchecked", "rawtypes" }) public RedisTemplate
5.7、序列化工具public class FastJsonRedisSerializer
使公用返回对象枚举类和自定义异常方便扩展
public interface CommonResp { Integer getCode(); String getMsg(); CommonResp setMsg(String msg);}
1.2、公用返回对象@Datapublic class RespBean implements Serializable { private static final long serialVersionUID = 1L; private Integer code; private String msg; private Object obj; public RespBean(RespBeanEnum respBeanEnum, Object obj) { this.code = respBeanEnum.getCode(); this.msg = respBeanEnum.getMsg(); this.obj = obj; } public RespBean(RespBeanEnum respBeanEnum) { this.code = respBeanEnum.getCode(); this.msg = respBeanEnum.getMsg(); } public RespBean(RespBeanEnum respBeanEnum, String msg) { this.code = respBeanEnum.getCode(); this.msg = msg; } public RespBean() { this.code = RespBeanEnum.ERROR.getCode(); this.msg = RespBeanEnum.ERROR.getMsg(); } public RespBean(String msg) { this.code = RespBeanEnum.ERROR.getCode(); this.msg = msg; } //自定义的业务异常错误码和信息 public RespBean(ServicesException e) { this.code = e.getCode(); this.msg = e.getMsg(); }}
1.3、枚举类public enum RespBeanEnum implements CommonResp{ SUCCESS(200,"请求成功!"), ERROR(500,"服务器响应错误!"), USER_REGISTER_FAILED(1001, "注册失败"), USER_ACCOUNT_EXISTED(1002,"用户名已存在"), USER_ACCOUNT_NOT_EXIST(1003,"用户名不存在"), USERNAME_PASSWORD_ERROR(1004,"用户名或密码错误"), PASSWORD_ERROR(1005,"密码错误"), USER_ACCOUNT_EXPIRED(1006,"账号过期"), USER_PASSWORD_EXPIRED(1007,"密码过期"), USER_ACCOUNT_DISABLE(1008,"账号不可用"), USER_ACCOUNT_LOCKED(1009,"账号锁定"), USER_NOT_LOGIN(1010,"用户未登陆"), USER_NO_PERMISSIONS(1011,"用户权限不足"), USER_SESSION_INVALID(1012,"会话已超时"), USER_ACCOUNT_LOGIN_IN_OTHER_PLACE(1013,"账号超时或账号在另一个地方登陆"), TOKEN_VALIDATE_FAILED(1014,"Token令牌验证失败"), LIKE_ALREADY_GICED(1015,"请勿重复点赞"), PICTURE_UPLOAD_FAILED(2001,"上传图片失败"), GIVE_LIKE_FAILED(2002,"点赞失败"), PICTURE_LOAD_FAILED(2003,"图片加载失败"), UPDATE_USER_INFO_FAILED(2004,"修改用户信息失败"), UPDATE_USER_PASSWORD_FAILED(2005,"修改密码失败"), ; private Integer code; private String msg; RespBeanEnum(Integer code, String msg) { this.code = code; this.msg = msg; } @Override public Integer getCode() { return this.code; } @Override public String getMsg() { return this.msg; } @Override public CommonResp setMsg(String msg) { this.msg=msg; return this; }}
2、全局异常 2.1、自定义异常实现CommonResp接口,方便自定义异常后续修改错误信息
public class ServicesException extends RuntimeException implements CommonResp { private CommonResp commonResp; //直接接收RespBeanEnum的传参用于构造业务异常 public ServicesException(CommonResp commonResp) { super(); //调用父类的无参构造方法 this.commonResp = commonResp; } //接收自定义msg的方式构造业务异常 public ServicesException(String msg, CommonResp commonResp) { super(); this.commonResp = commonResp; this.commonResp.setMsg(msg); } @Override public Integer getCode() { return this.commonResp.getCode(); } @Override public String getMsg() { return this.commonResp.getMsg(); } @Override public CommonResp setMsg(String msg) { this.commonResp.setMsg(msg); return this; }}
2.2、全局异常处理器@RestControllerAdvice注解表示捕获控制层抛出的异常
@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常
(图片来源:https://blog.csdn.net/weixin_43702146/article/details/118606502)
因为使用了@RestControllerAdvice注解,自动去捕获控制层抛出的异常,AuthenticationException异常和AccessDeniedException异常也被捕获了,但是我不想在这里处理,所以将这两个异常往外抛给失败处理器去处理。
@RestControllerAdvice //捕获controller层的异常public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(value = ServicesException.class) public RespBean servicesExceptionHandler(ServicesException e){ logger.error("发生业务异常! 原因是:{}",e.getMsg()); return new RespBean(e); } @ExceptionHandler(value = Exception.class) public RespBean exceptionHandler(Exception e){ logger.error("未知异常! 原因是:",e); return new RespBean(); } @ExceptionHandler(value = AuthenticationException.class) public void accountExpiredExceptionHandler(AuthenticationException authException){ throw authException; } //将 AccessDeniedException 异常往上抛,让授权处理器去处理 @ExceptionHandler(value = AccessDeniedException.class) public void accessDeniedExceptionHandler(AccessDeniedException accDenException){ throw accDenException; }}
五、登陆认证1、登陆模块UsernamePasswordAuthenticationToken继承了AbstractAuthenticationToken抽象类,AbstractAuthenticationToken抽象类实现了Authentication接口
认证流程:
1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层
2、Service层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象
3、然后调用AuthenticationManager的authenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
4、AbstractUserDetailsAuthenticationProvider的authenticate()方法中调用了抽象方法retrieveUser()方法
5、DaoAuthenticationProvider在重写方法retrieveUser()里调用了loadUserByUsername()方法,自定义AuthUserDetailsServiceImpl类实现UserDetailsService接口,重写loadUserByUsername()方法
6、在loadUserByUsername()方法中,会查询用户和角色,然后返回UserDetails对象
1.1、控制器LoginController包括登陆和登出功能
@RestControllerpublic class LoginController { @Autowired private LoginService loginService; @PostMapping("/doLogin") public RespBean doLogin(@RequestBody LoginDTO loginDTO){ return loginService.doLogin(loginDTO); } @RequestMapping("/doLogout") public RespBean doLogout(){ return loginService.doLogout(); }}
1.2、业务层1.2.1、LoginServiceService层创建UsernamePasswordAuthenticationToken对象,把用户名和密码封装成Authentication对象
public interface LoginService { RespBean doLogin(LoginDTO loginDTO); RespBean doLogout();}
1.2.2、LoginServiceImpl这里的 AuthenticationManager 需要在 SpringSecurity 中使用 authenticationManagerBean() 方法才能调用AuthenticationManager的authenticate()方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider中重写了authenticate()方法
这里把生成的Token和查询到的用户信息存到Redis中,方便后续使用
@Servicepublic class LoginServiceImpl implements LoginService { private static final Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class); @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private TUserService userService; @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private BCryptPasswordEncoderUtil passwordEncoder; @Autowired private RedisCache redisCache; @Override public RespBean doLogin(LoginDTO loginDTO) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if (Objects.isNull(authenticate)) { //用户名密码错误 throw new ServicesException(RespBeanEnum.USERNAME_PASSWORD_ERROR); } AuthUser authUser = (AuthUser) authenticate.getPrincipal(); String username = authUser.getTUser().getUsername(); String token = JwtUtil.createJWT(username); //把token和用户信息存到redis中 redisCache.setCacheObject("Token_" + username, token); redisCache.setCacheObject("UserDetails_" + username, authUser); //将用户存入上下文中 SecurityContextHolder.getContext().setAuthentication(authenticationToken); Map
public interface TUserService extends IService
TUserMapper需要继承baseMapper才能使用selectOne()这个方法
@Servicepublic class TUserServiceImpl extends ServiceImpl
@Data@AllArgsConstructor //全参构造@NoArgsConstructor //无参构造public class AuthUser implements UserDetails { private TUser tUser;// @JSonField(serialize = false) private Collection<? extends GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return tUser.getPassword(); } @Override public String getUsername() { return tUser.getUsername(); } // 账户是否未过期 @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、实现UserDetailsService 接口重写UserDetailsService接口的 loadUserByUsername()方法,在loadUserByUsername()方法中,会查询用户和权限(这里没有权限表,所以查的是角色),然后返回UserDetails对象
@Service(value = "userDetailsService")public class AuthUserDetailsServiceImpl implements UserDetailsService { private static final Logger logger = LoggerFactory.getLogger(AuthUserDetailsServiceImpl.class); @Autowired private TUserService userService; @Autowired private TRoleService roleService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { TUser user = userService.getUserByUserName(username); if (user == null) { //用户名不存在 throw new ServicesException(RespBeanEnum.USER_ACCOUNT_NOT_EXIST); } else { //查找角色,实际应该查询权限,但我数据库没有设计所以就查角色就好了 List
@Mapperpublic interface TUserMapper extends baseMapper
@Componentpublic class MyOncePerRequestFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(MyOncePerRequestFilter.class); @Value("${jwt.tokenHeader}") private String header; @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // header的值是在yml文件中定义的 “Authorization” String token = request.getHeader(header); System.out.println("MyOncePerRequestFilter-token = " + token); if (!StrUtil.isEmpty(token)) { String username = null; try { Claims claims = JwtUtil.parseJWT(token); username = claims.getSubject(); } catch (Exception e) { e.printStackTrace();// throw new ServicesException("非法Token,请重新登陆", RespBeanEnum.ERROR); WriteJSON(request,response,new RespBean(RespBeanEnum.ERROR,"非法Token,请重新登陆")); return; } String redisToken = redisCache.getCacheObject("Token_" + username); System.out.println("MyOncePerRequestFilter-redisToken = " + redisToken); if (StrUtil.isEmpty(redisToken)) { //token令牌验证失败// throw new ServicesException(RespBeanEnum.TOKEN_VALIDATE_FAILED); //输出JSON WriteJSON(request,response,new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED)); return; } //对比前端发送请求携带的的token是否与redis中存储的一致 if (!Objects.isNull(redisToken) && redisToken.equals(token)) { AuthUser authUser = redisCache.getCacheObject("UserDetails_" + username); System.out.println("MyOncePerRequestFilter-authUser = " + authUser); if (Objects.isNull(authUser)) {// throw new ServicesException(RespBeanEnum.USER_NOT_LOGIN); WriteJSON(request,response,new RespBean(RespBeanEnum.USER_NOT_LOGIN)); return; } UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } chain.doFilter(request, response); } private void WriteJSON(HttpServletRequest request, HttpServletResponse response, Object obj) throws IOException, ServletException { //这里很重要,否则页面获取不到正常的JSON数据集 response.setContentType("application/json;charset=UTF-8"); //跨域设置 response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Method", "POST,GET"); //输出JSON PrintWriter out = response.getWriter(); out.write(JSON.toJSONString(obj)); out.flush(); out.close(); }}
2.2、SpringSecuity配置类在配置类中使用addFilterBefore()方法让认证过滤器MyOncePerRequestFilter添加在UsernamePasswordAuthenticationFilter这个过滤器前面
@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyOncePerRequestFilter myOncePerRequestFilter; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { //1、关闭csrf,关闭Session http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //2、设置不需要认证的URL http .authorizeRequests() //允许未登录的用户进行访问 .antMatchers("/doLogin").anonymous() //其余url都要认证才能访问 .anyRequest().authenticated(); //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器 http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class); }}
六、授权使用 @PreAuthorize注解需要在SpringSecurity配置类中使用下面的语句才能开启方法级的安全
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RestController@RequestMapping("/user")public class TUserController { @RequestMapping("/hello") //对于hasRole这个方法来讲,ROLE_ 加不加都可以,它的方法会自动判断的 @PreAuthorize("hasRole('ROLE_user')") public String test() { return "Hello Login Success!"; }}
这样就可以了,因为前面已经写好了一些关联的代码,所以在访问该URL的时候,会执行hasRole()这个方法,然后查询AuthUser类(AuthUser类就是实现了UserDetails接口的实现类)中的属性authorities,只要authorities中包含"ROLE_user",则该用户就可以访问这个URL,否则会报错,提示权限不足。
七、自定义失败处理器 1、认证失败处理器注意访问一些需要认证后才能访问的URL时,记得带上token和content-type。
我这里的token的key是Authorization,这个是在application.yml文件中定义的,可以自行修改
继承自定义的JSON格式输出类JSONAuthentication输出JSON格式,同时在里面判断是什么异常做针对性输出
@Componentpublic class MyAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(MyAuthenticationEntryPoint.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { //用户未登录或者身份校验失败// RespBean respBean = new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED);// this.WriteJSON(request, response, respBean); RespBean respBean; if (authException instanceof AccountExpiredException) { //账号过期 respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_EXPIRED); } else if (authException instanceof InternalAuthenticationServiceException) { //用户不存在 respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_NOT_EXIST); } else if (authException instanceof BadCredentialsException) { //用户名或密码错误(也就是用户名匹配不上密码) respBean = new RespBean(RespBeanEnum.USERNAME_PASSWORD_ERROR); } else if (authException instanceof CredentialsExpiredException) { //密码过期 respBean = new RespBean(RespBeanEnum.USER_PASSWORD_EXPIRED); } else if (authException instanceof DisabledException) { //账号不可用 respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_DISABLE); } else if (authException instanceof LockedException) { //账号锁定 respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_LOCKED); } else { //其他错误 respBean = new RespBean(RespBeanEnum.USER_NOT_LOGIN); } //打印错误 logger.error(String.valueOf(authException)); //输出 this.WriteJSON(request, response, respBean); }}
2、权限不足处理器@Componentpublic class MyAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { //用户权限不足 RespBean respBean = new RespBean(RespBeanEnum.USER_NO_PERMISSIONS); //输出 this.WriteJSON(request, response, respBean); }}
3、SpringSecurity配置在configure方法中配置失败处理器
@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyOncePerRequestFilter myOncePerRequestFilter; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { //1、关闭csrf,关闭Session http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //2、设置不需要认证的URL http .authorizeRequests() //允许未登录的用户进行访问 .antMatchers("/doLogin").anonymous() //其余url都要认证才能访问 .anyRequest().authenticated(); //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器 http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class); //4、异常处理 http .exceptionHandling() //认证失败处理器 .authenticationEntryPoint(myAuthenticationEntryPoint) //权限不足处理器 .accessDeniedHandler(myAccessDeniedHandler); }}
八、跨域 1、编写配置类@Configurationpublic class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") //允许任何域名 .allowedOriginPatterns("*") //允许任何方法 .allowedMethods("PUT", "DELETE", "GET", "POST", "OPTIONS") //允许任何头 .allowedHeaders("*") //暴露头 .exposedHeaders("access-control-allow-headers", "access-control-allow-methods", "access-control-allow-origin", "access-control-max-age", "X-frame-Options") // 是否允许证书(cookies) .allowCredentials(true) .maxAge(3600); }}
2、在SpringSecurity配置类中配置在配置类的configure()方法中开启允许跨域
@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyOncePerRequestFilter myOncePerRequestFilter; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { //1、关闭csrf,关闭Session http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //2、设置不需要认证的URL http .authorizeRequests() //允许未登录的用户进行访问 .antMatchers("/doLogin").anonymous() //其余url都要认证才能访问 .anyRequest().authenticated(); //3、在UsernamePasswordAuthenticationFilter前添加认证过滤器 http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class); //4、异常处理 http .exceptionHandling() //认证失败处理器 .authenticationEntryPoint(myAuthenticationEntryPoint) //权限不足处理器 .accessDeniedHandler(myAccessDeniedHandler); //5、允许跨域 http.cors(); }}