1.登陆接口Controller
接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。
package com.lyd.springsecurity1.controller;@RestControllerpublic class LoginController {//具体的认证,生成jwt,存入redis的方法,写到service层,LoingService是自己写的Service接口@Autowiredprivate LoginService loginService;//这个是登陆接口@PostMapping ("/login")public ResponseResult login(@RequestBody User user){return loginService.login(user);}//这个是登出接口,登出的时候不需要什么参数,请求头带上token就行@GetMapping ("/logout")public ResponseResult login(){return loginService.logout();}}
2.SpringSecurity配置类
package com.lyd.springsecurity1.config;//限制访问资源需要的权限,就是访问控制器需要验证权限,开启这个功能@EnableGlobalMethodSecurity(prePostEnabled = true)@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {//创建BCryptPasswordEncoder注入容器//这个是用来验证加密密码的,可以自己写一个自己的加密方式@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}//这个是用来在service层验证用户的,通过AuthenticationManager的authenticate方法来进行用户认证@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//token验证的过滤器//我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;//注入认证异常处理和授权异常处理@Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint;@Autowiredprivate AccessDeniedHandler accessDeniedHandler;//配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问,这个antMatchers()可以写多个,链式编程.antMatchers("/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//禁止默认的登出http.logout().disable();//允许跨域http.cors();//添加一个过滤器jwtAuthenticationTokenFilter,添加在UsernamePasswordAuthenticationFilter过滤器之前//jwtAuthenticationTokenFilter是自己写的过滤器http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);//配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)//配置认证异常处理器.accessDeniedHandler(accessDeniedHandler);//配置授权异常处理器}}
3.Service层认证用户
接口
package com.lyd.springsecurity1.service;public interface LoginService {//User是用户的实体类ResponseResult login(User user);ResponseResult logout();}
实现这个接口。规范
package com.lyd.springsecurity1.service;@Servicepublic class LoginServiceImpl implements LoginService{//这个在配置文件中注入过了,用来进行用户认证@Autowiredprivate AuthenticationManager authenticationManager;//把Redis的基本操作都放在RedisCache工具类中,直接调用方便@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(User user) {//看:注解1UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);//如果认证没通过,给出对应的提示,抛出异常,交给全局异常捕获,认证没通过authenticate就是nullif(Objects.isNull(authenticate)){throw new RuntimeException("登陆失败");}//如果认证通过了,authenticate不是null,使用userid生成一个jwt,jwt存入返回结果里面,返回出去//LoginUser是自己写的登陆用户类LoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userid = loginUser.getUser().getId().toString();//这里用的JWT工具类String jwt = JwtUtil.createJWT(userid);//把完整的用户信息存入Redis,usrid作为keyredisCache.setCacheObject("login:"+userid,loginUser);Map<String,String> map = new HashMap<>();map.put("token",jwt);//返回统一格式的响应体return new ResponseResult(200,"登陆成功",map);}//登出@Overridepublic ResponseResult logout() {//获取SecurityContextHolder中的用户idUsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();String id = loginUser.getUser().getId().toString();//删除redis中的值//如果两个人同时登陆一个账号,其中一个人退出登陆了,如果用id作为键的话,另外一个人也被迫退出了,可以用uuid之类的redisCache.deleteObject("login:"+id);return new ResponseResult(200,"退出成功");}}
:::warning
注解1:
调用AuthenticationManager的authenticate方法进行用户认证,这个在Security配置里面注入的。
而authenticate方法传入一个Authentication类型的对象,传入它的子类UsernamePasswordAuthenticationToken,把前端传入的账号密码放进去。
这个authenticate就完成了登陆时候的验证?账号密码在数据库里面的啊,所以这个方法一定包含了去数据库查询账号密码验证的一系列步骤,甚至LoginUser这个我自己写的对象也在里面。
所以还有个自定义东西需要我们去完成查数据库,LoginUser封装等。我们写好后替代原生的方法,就是把写好的东西放入容器,运行的时候authenticate就用我们自己写的那个东西。
SpringSecurity认证
看认证流程图
:::
4.实现UserDetailsService
根据认证流程图,我们需要自定义的去数据库查询用户信息,创建一个类实现UserDetailsService接口,重写其中的loadUserByUsername方法。
package com.lyd.springsecurity1.service;//自己实现一个UserDetailsService,重写loadUserByUsername方法//这里自定义的验证@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询用户User user = userMapper.queryUser(username);//如果查出来的user为空,就抛出异常,SpringSecurity里面有个过滤器可以监视其他过滤器抛出的异常if(Objects.isNull(user)){throw new UsernameNotFoundException("用户名没有找到");}//查询对应的权限信息List<String> permissionKeyList = userMapper.queryPermissions(user.getId());//最终要把用户信息,和权限信息,封装成UserDetails返回。这是一个接口,需要自己写一个实现类实现这个接口return new LoginUser(user,permissionKeyList);}}
5.实现UserDetails
这个是用来装用户数据的,是要给接口,我们要实现这个接口
package com.lyd.springsecurity1.domain;public class LoginUser implements UserDetails {//自己定义用户属性private User user;//存储权限信息,权限信息就是一个字符串列表,一个字符串一个权限private List<String> permissions;public LoginUser() {}public LoginUser(User user, List<String> permissions) {this.user = user;this.permissions = permissions;}public List<String> getPermissions() {return permissions;}public void setPermissions(List<String> permissions) {this.permissions = permissions;}public User getUser() {return user;}public void setUser(User user) {this.user = user;}//存储SpringSecurity所需要的权限信息的集合//这个集合在序列化时,不存入redis@JSONField(serialize = false)private List<GrantedAuthority> authorities;/** 最终认证都是通过UserDetails这个类的下面这些方法进行认证,比如获取账号密码什么的* 你可以自己添加一个其他的* 所以这些方法都是根据上面的user对象里面的属性,而得到返回结果* */@Override//得到权限信息public Collection<? extends GrantedAuthority> getAuthorities() {if(authorities!=null){return authorities;}//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中for(String p:permissions){authorities.add(new SimpleGrantedAuthority(p));}return authorities;}@Override//得到用户密码public String getPassword() {return user.getPassword();}@Override//得到用户名public String getUsername() {return user.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;}}
6.过滤器认证token
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。
使用userid去redis中获取对应的LoginUser对象。
然后封装Authentication对象存入SecurityContextHolder
package com.lyd.springsecurity1.filter;@Componentpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {//redis工具类@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取token,这个是请求头里面是啥就是啥,请求头不一定就是tokenString token = request.getHeader("token");if(!StringUtils.hasText(token)){//请求头里面token都没有,那只有放行了.因为后面的过滤器会判断是不是认证状态,有可能登陆的时候没有tokenfilterChain.doFilter(request,response);return;}//解析token,需要jwt的依赖和工具类String userid;try {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {//把数据直接响应给前端,用了WebUtils工具类,看准备工作那一章ResponseResult result = ResponseResult.errorResult(401, "token非法请重新登陆");WebUtils.renderString(response, JSON.toJSONString(result));return;}//从redis中获取用户信息String redisKey = "login:"+userid;LoginUser loginUser = redisCache.getCacheObject(redisKey);if(Objects.isNull((loginUser))){//redis中没有获取这个用户的信息,要么过期了要么一开始就没有这个数据,统一就是用户没有登陆ResponseResult result = ResponseResult.errorResult(401, "用户未登录请重新登陆");WebUtils.renderString(response, JSON.toJSONString(result));}//获取权限信息封装到Authentication中//在第3步和第4步,实现UserDetailsService和UserDetails中,好像设置了权限。下面就不用传入权限了,loginUser里面有。//存入SecurityContextHolderif (Objects.nonNull(loginUser)) {//这个玩意上面有提到UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);}//token解析完了,就要放行了,进下一个过滤器,token解析查出来的用户数据,存放在一个公共的SecurityContextHolder里面,其他过滤器可以拿到filterChain.doFilter(request,response);}}
7.限制访问资源所需要的权限
在每个控制器上加上注解@PreAuthorize
里面的参数:
package com.lyd.springsecurity1.controller;@RestControllerpublic class HelloController {//表示需要test权限才能访问这个接口@PreAuthorize("hasAuthority('test')")@GetMapping("/hello")public String hello(){return "返回你一个hello";}}
8.请求异常处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。
要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
:::success
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
:::
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
配置在第3步配置类里面有写
package com.lyd.springsecurity1.handler;//关于认证异常的处理@Componentpublic class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {//如果异常了,会返回非200的响应吗,不返回内容。我们要统一返回一个响应格式,把失败的信息也返回,ResponseResult是自己定义的响应体ResponseResult result =new ResponseResult(401,"用户认证失败");//自己写了一个工具类,设置响应,在springsecurity准备工作里面这个工具类WebUtils.renderString(response,JSON.toJSONString(result));}}
package com.lyd.springsecurity1.handler;//关于授权异常处理@Componentpublic class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult result =new ResponseResult(403,"你的权限不足");WebUtils.renderString(response, JSON.toJSONString(result));}}
