一、引入依赖

        <!-- 有默认的登录页面/login,有默认的账号密码,默认的认证,默认退出界面/logout -->
	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

直接引入后,需要解决的问题:

  • SpringSecurity默认提供了一个账号密码(账号是user,密码会随机生成一个),在登录时,(框架)会自动比较。但是我们需要自己从数据库中查询出账号密码,再(让框架自动)比较。
  • SpringSecurity默认提供了一个/login请求下的登录页面。现在是前后端分离程序,需要我们自定义/login请求和登录页面。
  • SpringSecurity框架如何融入JWT,token。
  • SpringSecurity框架如何进行授权

二、编写配置类

注入密码加密器、自定义认证管理器、重写登录请求和登录过滤器。

//启用授权检查
@EnableGlobalMethodSecurity(prePostEnabled=true)
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    JwtAuthentiactionFilter jwtAuthentiactionFilter;
    //注入密码加密器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //注入认证管理器,自定义实现认证过程
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    //重写SpringSecurity的默认配置文件(里面包含了直接跳转到默认登录页面的配置),放行自定义login请求,拦截其他请求
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //csrf跨站攻击
                .csrf().disable()
                //禁用Session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //JWTFilter在Security内置过滤器之前执行
                .addFilterBefore(jwtAuthentiactionFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                //特殊路径放行,指定特殊路径可以匿名访问
                .antMatchers("/user/login").anonymous()
                .anyRequest().authenticated();
    }
}

三、创建登录实体类

@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {

    private Integer uid;
    private String uname;
    private String password;
}

四、自定义数据库查询方法

继承UserDetailsService接口,自定义数据库查询。

@Service
public class LoginUserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user=null;
        List<String> permissions=new ArrayList<>();
        //从数据库根据用户名查询用户信息
        if("a".equals(username)){
          user=new User(1,"a","$2a$10$CEhZfCMH2KzVVT0Ex6UoDeDLyZ51WevCXKshr3X63kOzrjCYW70d2");
            //从数据库根据用户信息查询用户的权限
            permissions.add("test");
            permissions.add("add");
        }else if("b".equals(username)){
            user=new User(2,"b","$2a$10$SXKszBA016Vpc97fbWh5L.1MrEHF0k5jd5qyrbWkc1feuDSeGrNk6");
            permissions.add("test");
        }else{
            throw new RuntimeException("用户名不存在");
        }
        //UserDetails存入redis中
        UserDetails userDetails=new LoginUserDetails(user,permissions);
        return userDetails;
    }
}

五、封装数据库查询的数据

@NoArgsConstructor
@AllArgsConstructor
@Data
public class LoginUserDetails implements UserDetails {

    //从数据库查询的用户信息
    private User user;
    private List<String> permissions;

    //后面授权要使用的
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities=new ArrayList<>();
        permissions.stream()
                .forEach(s -> {
                    SimpleGrantedAuthority authority = new SimpleGrantedAuthority(s);
                    authorities.add(authority);
                });
        return authorities;
    }

    //与前端传递过来的密码自动比较,一定要获取查询密码
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    //这些判断以后再Filter通过token实现,都让他们返回true
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

六、controller层定义login登录请求

@RestController
public class UserController {

    @Autowired
    UserService userService;

    @RequestMapping("/user/login")
    public ResponseResult login(@RequestBody User user, HttpServletResponse response){
        //验证账号密码,底层会校验
        //底层校验通过,发token,写入响应头,响应前端
        String token = userService.login(user);
        response.setHeader("token", token);
        response.setHeader("Access-Control-Expose-Headers", "token");
        return new ResponseResult(200,"ok","login suceess");
    }
}

七、service层自定义认证管理器

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    AuthenticationManager authenticationManager;

    @Override
    public String login(User user) {
        //用户名密码,校验通过,返回token
        //密码比较,不需要再去实现,底层认证器已经实现了
        //Object principal,     身份信息:用户名,用户对象
        // Object credentials,  凭证:密码
        //获取前端的用户名密码,封装成UsernamePasswordAuthenticationToken对象
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getUname(), user.getPassword());
        //调用认证管理器的认证方法,认证失败底层自动抛异常
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //认证成功,返回值获取身份信息,就是LoginUserDetails对象
        LoginUserDetails userDetails = (LoginUserDetails) authenticate.getPrincipal();
        System.out.println("验证器验证通过,返回Principal身份信息:"+userDetails);
        //认证成功,生成token
        return JwtUtil.createRereshToken(userDetails.getUser().getUid()+"",user.getUname());
    }
}

不需要创建mapper,SpringSecurity会根据写好的查询逻辑,自动查询并和传过来的前端对象自动进行比较。

八、创建JWT的token登录拦截器

//OncePerRequestFilter是Spring框架封装的过滤器
@Component
public class JwtAuthentiactionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //请求携带token,进行token校验
        String token = request.getHeader("token");
        if(StringUtils.isEmpty(token)){
            //没有token,放行其实是拦截:SpringSecurity框架底层有拦截器
            filterChain.doFilter(request, response);
            return;
        }
        if(!JwtUtil.validate(token)){
            throw new RuntimeException("token无效");
        }
        //token有效,记录到Security上下文中,告诉后面的拦截器,可以放行
        //可以从redis获取完整的用户信息
        //现在没有存,还是模拟数据库查询
        String username = JwtUtil.getEname(token);
        User user=null;
        List<String> permissions=new ArrayList<>();
        //从数据库根据用户名查询用户信息
        if("a".equals(username)){
            user=new User(1,"a","$2a$10$CEhZfCMH2KzVVT0Ex6UoDeDLyZ51WevCXKshr3X63kOzrjCYW70d2");
            permissions.add("test");
            permissions.add("add");
        }else if("b".equals(username)){
            user=new User(2,"b","$2a$10$SXKszBA016Vpc97fbWh5L.1MrEHF0k5jd5qyrbWkc1feuDSeGrNk6");
            permissions.add("test");
        }else{
            throw new RuntimeException("用户名不存在");
        }
        LoginUserDetails userDetails=new LoginUserDetails(user,permissions);
        //一定要用三个的参数:第三个参数是权限,super.setAuthenticated(true);
        //Object principal,     身份信息
        // Object credentials,  凭证,我们使用token,null不影响使用
        //Collection<? extends GrantedAuthority> authorities,要填入查询到的权限
        Authentication authentication=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        //将通过token验证的信息,写入sercuity上下文,权限框架的过滤器就不会再拦截了
        SecurityContextHolder.getContext().setAuthentication(authentication);

        System.out.println("JWT通过");
        filterChain.doFilter(request, response);
    }
}