JAVA——springSecurity——自定义配置的一些补充:Anonymous匿名用户、重写loadUserByUsername()方法、自定义WebSecurityConfig配置等

三、一些细节补充

(1)Anonymous匿名用户

未登录的情况下发起一个非认证请求,系统会自动生成一个匿名对象anonymoususer,但这个匿名用户不包含任何权限

AnonymousAuthenticationFilter类——doFilter()方法

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    //获取当前 spring security的上下文对象,从上下文对象中获取一个对象authentication(Token - 标志,象征)。
    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        //如果为空,说明用户未登录(用户如果登录会往上下文中存入这个东西),那就调用createAuthentication()方法创建一个匿名用户,并把这个匿名用户存入上下文
SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Populated SecurityContextHolder with anonymous token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
            }
        } else if (this.logger.isDebugEnabled()) {
            this.logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
        }

        chain.doFilter(req, res);
}

AnonymousAuthenticationFilter类——createAuthentication()方法

protected Authentication createAuthentication(HttpServletRequest request) {
    //创建一个匿名对象auth
    AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(this.key, this.principal, this.authorities);
    auth.setDetails(this.authenticationDetailsSource.buildDetails(request));
    return auth;
}

(2)重写loadUserByUsername()方法

要想从自定义的数据库中读取账号和密码,可以在我们自定义的配置类WebSecurityConfig中配置,只需要两步:
1.重新定制DaoAuthenticationProvider
2.指定认证管理器为自定义的管理器
3.在用户业务实现类UserServiceImpl中重写loadUserByUsername(username)方法,使用户名通过访问数据库得到
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserServiceImpl userServiceImpl;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存中配置用户名和密码
//        auth.inMemoryAuthentication().withUser("tom")
//                .password(passwordEncoder.encode("123")).roles();
//        auth.inMemoryAuthentication().withUser("admin")
//                .password(passwordEncoder.encode("admin")).roles();
        //2.指定认证管理器
        auth.authenticationProvider(getDaoAuthenticationProvider());
    }


    /**
     * 1.重新定制DaoAuthenticationProvider
     * @return
     */
    @Bean
    public DaoAuthenticationProvider getDaoAuthenticationProvider(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //设置用户未找到的异常不隐藏
        provider.setHideUserNotFoundExceptions(false);
        //设置认证管理器使用userServiceImpl对象
        provider.setUserDetailsService(userServiceImpl);
        //设置认证管理器使用的密码管理对象
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }
}
/**
 * <p>
 *  服务实现类
 * </p>
 * 实现了UserDetailsService接口,重写它的loadUserByUsername方法
 * 使springSecurity从内存中读取账号密码改成从我们自定义的数据库中读取帐号密码
 * @author z
 * @since 2022-07-05
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService, UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 重写了UserDetailsService接口的loadUserByUsername方法
     * 从数据库读取用户名和密码
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException{
        //3.访问数据库,根据用户名查询用户对象
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        User user = userMapper.selectOne(wrapper);
        if(user==null){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //查询用户的权限集
        List<String> permList = userMapper.getPerCodesByPerm(user.getUsername());

        //封装用户的权限集
        List<GrantedAuthority> authorities = new ArrayList<>();
        permList.forEach(perm->{
            authorities.add(new SimpleGrantedAuthority(perm));
        });
        Boolean isEnabled = true;
        Boolean isAccountNonExpired = true;
        Boolean isCredentialsNonExpired = true;
        Boolean isAccountNonLocked = true;

        if(user.getStatus().equals("1")){
            //用户不可用
            isEnabled = false;
        }else if(user.getStatus().equals("2")){
            //账户已过期
            isAccountNonExpired=false;
        }else if(user.getStatus().equals("3")){
            //凭据已过期
            isCredentialsNonExpired=false;
        }else if(user.getStatus().equals("4")){
            //账户已锁定
            isAccountNonLocked=false;
        }

        UserDetails userDetails =
                new org.springframework.security.core.userdetails.User(
                        user.getUsername(),
                        passwordEncoder.encode(user.getPassword()),
                        isEnabled,
                        isAccountNonExpired,
                        isCredentialsNonExpired,
                        isAccountNonLocked,
                        authorities);

        return userDetails;
    }
}

(3)自定义响应结果ResponseResult工具类

@Data
public class ResponseResult<T> {
    private int status;
    private String msg;
    private T data;

    public ResponseResult(){}

    public ResponseResult(int status, String msg){
        this.status = status;
        this.msg = msg;
    }
    public ResponseResult(T data, String msg, int status){
        this(status,msg);
        this.data = data;
        this.msg = msg;
    }

    public static ResponseResult ok(){
        ResponseResult result = new ResponseResult();
        result.setStatus(ResultCode.SUCCESS.getCode());
        result.setMsg(ResultCode.SUCCESS.getMessage());
        return result;
    }

    public static ResponseResult error(ResultCode resultCode){
        ResponseResult result = new ResponseResult();
        result.setStatus(resultCode.getCode());
        result.setMsg(resultCode.getMessage());
        return result;
    }

    public static ResponseResult<Void> SUCCESS = new ResponseResult<>(200,"成功");
    public static ResponseResult<Void> INTEVER_ERROR = new ResponseResult<>(500,"服务器错误");
    public static ResponseResult<Void> NOT_FOUND = new ResponseResult<>(404,"未找到");

}

(4)自定义状态码和对应msg工具类ResultCode

public enum ResultCode {

    /* 成功 */
    SUCCESS(200, "成功"),

    /* 默认失败 */
    COMMON_FAIL(999, "失败"),

    /* 参数错误:1000~1999 */
    PARAM_NOT_VALID(1001, "参数无效"),
    PARAM_IS_BLANK(1002, "参数为空"),
    PARAM_TYPE_ERROR(1003, "参数类型错误"),
    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    /* 用户错误 */
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "您的登录已经超时或者已经在另一台机器登录,您被迫下线"),
    TOKEN_IS_NULL(2010,"TOKEN为空"),
    TOKEN_INVALID_EXCEPTION(2011,"TOKEN非法"),

    /* 业务错误 */
    NO_PERMISSION(4001, "没有权限"),

    /*部门错误*/
    DEPARTMENT_NOT_EXIST(5007, "部门不存在"),
    DEPARTMENT_ALREADY_EXIST(5008, "部门已存在"),

    /*运行时异常*/
    ARITHMETIC_EXCEPTION(9001,"算数异常"),
    NULL_POINTER_EXCEPTION(9002,"空指针异常"),
    ARRAY_INDEX_OUTOfBOUNDS_EXCEPTION(9003,"数组越界");


    ResultCode(Integer code, String message){
        this.code = code;
        this.message = message;
    }

    private Integer code;
    public Integer getCode() {
        return code;
    }

    private String message;
    public String getMessage() {
        return message;
    }

}

(5)自定义WebSecurityConfig配置

主要有三项:
1.SpringSecurity 自带HttpBasic基础认证模式
2.默认formLogin表单模式
3.自定义formLogin表单模式
5-1 HttpBasic模式登录认证

SpringSecurity 自带一种基础认证模式

实现方式:创建WebSecurityConfig配置类

/**
 * Spring Securtiy配置类
 */
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //开启httpBasci模式登录认证
        http.httpBasic()
                //每个模块配置使用and结尾
                .and()
                //配置路径拦截,表明路径访问所对应的权限,角色,认证信息
                .authorizeRequests()
                .anyRequest()
                //所有请求都需要登录认证才能访问
                .authenticated();
    }
}
5-2 默认formLogin表单模式

注释掉上面的配置类,通过applicaton.yml配置用户名与密码

spring:
    security:
            user:
                name: tom
                password: tom
5-3 自定义formLogin表单模式

继承WebSecurityConfigurerAdapter类,实现它的三个configure方法

//@Configuration
@EnableWebSecurity //涵盖了 @Configuration 注解
//@EnableGlobalMethodSecurity(jsr250Enabled = true)
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启Security注解鉴权的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserServiceImpl userServiceImpl;
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Autowired
    private JwtTokenAuthenticationFilter jwtTokenAuthenticationFilter;
    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在内存中配置用户名和密码
//        auth.inMemoryAuthentication().withUser("tom")
//                .password(passwordEncoder.encode("123")).roles();
//        auth.inMemoryAuthentication().withUser("admin")
//                .password(passwordEncoder.encode("admin")).roles();
        //从指定的数据库读取账号密码
//        auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder);
        //指定认证管理器
        auth.authenticationProvider(getDaoAuthenticationProvider());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //开启httpBasic认证
//        http.httpBasic()
//                //每个模块配置使用and结尾
//                .and()
//                //配置路径拦截,表明路径访问所对应的权限,角色,认证信息
//                .authorizeRequests()
//                .anyRequest()
//                //所有请求都需要登录认证才能访问
//                .authenticated();
        //http.httpBasic().and().authorizeRequests().anyRequest().authenticated(); //关闭httpBasic认证

        //开启自定义formLogin表单认证
        //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
        http.authorizeRequests().antMatchers("/login", "/login.html")
                .permitAll().anyRequest().authenticated().and().
                // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
                        formLogin().loginPage("/login.html").loginProcessingUrl("/login")
                //登录表单form中密码输入框input的name名,不修改的话默认是password
                .usernameParameter("username").passwordParameter("password")
                //登录认证成功后默认转跳的路径
//                .defaultSuccessUrl("/home")
                //登陆成功后不跳转,返回json数据
                .successHandler(myAuthenticationSuccessHandler)
                //登录认证失败后,被放行的请求
                //注意,此请求不能为error,因为已经被springSecurity定义
//                .failureUrl("/gotoError1").permitAll();
                //登录认证失败后,不跳转,返回json数据
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(myAccessDeniedHandler)
                        .and().logout().logoutSuccessHandler(myLogoutSuccessHandler);
        //将自定义的JwtTokenAuthenticationFilter插入到过滤器链的指定过滤器前面
        http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //关闭CSRF跨域
        http.csrf().disable();
        //关闭session最严格的策略
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    /**
     * 重新定制DaoAuthenticationProvider
     * @return
     */
    @Bean
    public DaoAuthenticationProvider getDaoAuthenticationProvider(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //设置用户未找到的异常不隐藏
        provider.setHideUserNotFoundExceptions(false);
        //设置认证管理器使用userServiceImpl对象
        provider.setUserDetailsService(userServiceImpl);
        //设置认证管理器使用的密码管理对象
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }
}
1. 配置需要直接放行的url,处理认证请求的路径
protected void configure(HttpSecurity http) throws Exception {
    //开启自定义表单模式登录认证
    //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
    http.authorizeRequests().antMatchers("/login", "/login.html")
        .permitAll().anyRequest().authenticated()
        .and().
        // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
        formLogin().loginPage("/login.html").loginProcessingUrl("/login")
        //登录表单form中密码输入框input的name名,不修改的话默认是password
        .usernameParameter("username").passwordParameter("password")
        //登录认证成功后默认转跳的路径
        .defaultSuccessUrl("/home");
    //关闭CSRF跨域攻击防御
    http.csrf().disable();
}
2. 配置PasswordEncoder密码加密

步骤1: 首先在启动类上创建 Bean注解方法

@SpringBootApplication
public class SpringbootSecurityDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootSecurityDemoApplication.class, args);
    }

    //创建一个PasswordEncoder加密器存入容器中
    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

步骤2: 在测试类中测试PasswordEncoder方法

@SpringBootTest
class SpringbootSecurityDemoApplicationTests {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    void contextLoads() {
        String pwd1 = passwordEncoder.encode("123");
        String pwd2 = passwordEncoder.encode("123");
        System.out.println(pwd1); //$2a$10$21Ae7HsPihdZNu.YWhqeHu4dJ5/45l5mpQxN8P3HWzPNlDeh84cm2
        System.out.println(pwd2); //$2a$10$000Misx3wpASVQkzz.iyK.q9L9qx7Thl7mey/UzoLEAvVJgenvGc.
        System.out.println(passwordEncoder.matches("123",pwd1));
        System.out.println(passwordEncoder.matches("123",pwd2));
        //$2a$10$s65QPon1AZTjZhQjlL.jF.8JBeIjdPLoL.UfZkDA5Uzv94yQhq.RG
        //$2a$10$/Rs7Zr41nC6UWgP/Ij0tnOnrBwMLjwRDYrGJrNMzMMlWQB5XgmBJe
    }
}

步骤3: 在配置类中使用加密的方式在内存中配置用户名和密码,修改类WebSecurityConfig

/**
 * Spring Securtiy配置类
 */
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("tom")
                .password(passwordEncoder.encode("123")).roles();
        auth.inMemoryAuthentication().withUser("admin")
                .password(passwordEncoder.encode("admin")).roles();
    }
    
    ...
}
3. 配置动态认证

即从自定义的数据库中查询数据进行用户登录,而不是从内存中

步骤1:编写UserServiceImpl实现UserDetailsService接口

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService, UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    /**
     * 根据用户名查询用户UserDetails
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //访问数据库,根据用户查询用户对象
        QueryWrapper<User>  wrapper = new QueryWrapper<>();
        wrapper.eq("username",username);
        User user = userMapper.selectOne(wrapper);
        if(user==null){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //封装用户的权限集
        List<GrantedAuthority> authorities = new ArrayList<>();

        //封装数据库存询的用户信息
        UserDetails userDetails =
                new org.springframework.security.core.userdetails.User(
                        user.getUsername(),user.getPassword(),authorities);

        return userDetails;
    }
}

步骤2:添加配置类相关代码

/**
 * Spring Securtiy配置类
 */
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserServiceImpl userServiceImpl;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceImpl).passwordEncoder(passwordEncoder);
    }
}
4. 配置异常处理

步骤1: 在配置类中配置异常的处理的url

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //开启自定义表单模式登录认证
        //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
        http.authorizeRequests().antMatchers("/login", "/login.html")
                .permitAll().anyRequest().authenticated()
                .and().
                // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
                        formLogin().loginPage("/login.html").loginProcessingUrl("/login")
                //登录表单form中密码输入框input的name名,不修改的话默认是password
                .usernameParameter("username").passwordParameter("password")
                //登录认证成功后默认转跳的路径
                .defaultSuccessUrl("/home")
                //登录认证失败后请求URL ,要放行
                .failureUrl("/error1").permitAll(); 
        //关闭CSRF跨域攻击防御
        http.csrf().disable();
    }

注意:此处不能指定/error ,因为这个 /error是security内置的请求处理

步骤2: 编写erro1处理器

@GetMapping("/error1")
public String error(HttpServletRequest request, HttpServletResponse response){
    return "error";
}

步骤3: 创建error.html模板页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
出错了: <p th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></p>
</body>
</html>

附加步骤4: 用户名找不到异常处理

由于spring security框架底层默认将UsernameNotFoundException设置为隐藏,而显示的是BadCredential异常,可以通过下面的方式配置实现

/**
 * Spring Securtiy配置类
 */
@Configuration  //配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserServiceImpl userServiceImpl;

    //重新定制DaoAuthenticationProvider
    @Bean
    public DaoAuthenticationProvider getDaoAuthenticationProvider(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //设置用户未找到异常不隐藏
        provider.setHideUserNotFoundExceptions(false);
        //设置认证管理器使用UserDetaisService对象--此处使用的是我们自定义的业务实现类
        provider.setUserDetailsService(userServiceImpl);
        //设置认证管理器使用的密码检验器对象
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //指定认证管理器
        auth.authenticationProvider(getDaoAuthenticationProvider());
    }
    
    ...
}
5. 配置前后端分离认证成败的处理
5-1 认证成功的处理

登录成功之后我们是跳转到/home控制器的,也就是跳转到home.html

但是在前后端分离的情况下,页面的跳转是交给前端去控制的,后端的控制器就不生效了,那我们应该如何实现让前端去跳转页面呢?

我们发现在认证成功后,执行的是AuthenticationSuccessHandler接口实现类:

默认为SimpleUrlAuthenticationSuccessHandler,如果定制自定义处理器,只需要实现该接口

/**
 * 自定义认证成功的处理器Handler
 */
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        ResponseResult<Void> result = ResponseResult.ok();
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out= response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
        out.flush();
        out.close();
    }
}

然后配置成功的处理器:

@Override
protected void configure(HttpSecurity http) throws Exception {
    //开启自定义表单模式登录认证
    //需要放行的url在这里配置,必须要放行/login和/login.html,不然会报错
    http.authorizeRequests().antMatchers("/login", "/login.html")
        .permitAll().anyRequest().authenticated()
        .and().
        // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
        formLogin().loginPage("/login.html").loginProcessingUrl("/login")
        //登录表单form中密码输入框input的name名,不修改的话默认是password
        .usernameParameter("username").passwordParameter("password")
        //登录认证成功后默认转跳的路径
        //.defaultSuccessUrl("/home")
        // 前后端分离认证成功的处理器 -输出json
        .successHandler(myAuthenticationSuccessHandler)
        .failureUrl("/error1").permitAll();
    //关闭CSRF跨域攻击防御
    http.csrf().disable();
}
5-2 认证失败的处理

同样的,有登录成功的处理器就有登录失败的处理器,但是登录失败的情况比较多,所以需要经过很多的判断,登录失败处理器主要用来对登录失败的场景(密码错误、账号锁定等…)做统一处理并返回给前台统一的json返回体

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response, AuthenticationException e)
                                        throws IOException, ServletException {
        //定义响应的结果对象
        ResponseResult<String> result = null;
        if(e instanceof UsernameNotFoundException){
            result = ResponseResult.error(ResultCode.USER_ACCOUNT_NOT_EXIST);
        }else if (e instanceof AccountExpiredException) {
            //账号过期
            result = ResponseResult.error(ResultCode.USER_ACCOUNT_EXPIRED);
        } else if (e instanceof BadCredentialsException) {
            //凭证不对   错误
            result = ResponseResult.error(ResultCode.USER_CREDENTIALS_ERROR);
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            result = ResponseResult.error(ResultCode.USER_CREDENTIALS_EXPIRED);
        } else if (e instanceof DisabledException) {
            //账号不可用
            result = ResponseResult.error(ResultCode.USER_ACCOUNT_DISABLE);
        } else if (e instanceof LockedException) {
            //账号锁定
            result = ResponseResult.error(ResultCode.USER_ACCOUNT_LOCKED);
        } else{
            result = ResponseResult.error(ResultCode.COMMON_FAIL);
        }

        response.setContentType("application/json;charset=UTF-8");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Method", "POST,GET");

        response.setContentType("application/json;charset=utf-8");
        PrintWriter out= response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
        out.flush();
        out.close();
    }
}

添加配置:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
              ....
                .successHandler(myAuthenticationSuccessHandler)
                // 前后端分离认证失败的处理器 -输出json
                .failureHandler(myAuthenticationFailureHandler);
                
        //关闭CSRF跨域攻击防御
        http.csrf().disable();
    }
6. 配置前后端分离用户未登录的处理

而在前后端分离的情况下(比如前台使用VUE或JQ等)我们需要的是在前台接收到"用户未登录"的提示信息,所以我们接下来要做的就是屏蔽重定向的登录页面,并返回统一的json格式的返回体。而实现这一功能的核心就是实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效

//自定义用户未登录的处理器
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    //重新定义未登录的处理
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseResult<Void> result = ResponseResult.error(ResultCode.USER_NOT_LOGIN);
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out= response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
        out.flush();
        out.close();
    }
}

配置用户未登录的处理

@Override
public void configure(WebSecurity web) throws Exception {
    super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...

        // 前后端分离认证成功的处理器 -输出json
        .successHandler(myAuthenticationSuccessHandler)
        // 前后端分离认证失败的处理器 -输出json
        .failureHandler(myAuthenticationFailureHandler)
        .and()
        // 前后端分离处理未登录请求
        .exceptionHandling()
        .authenticationEntryPoint(myAuthenticationEntryPoint);
    //关闭CSRF跨域攻击防御
    http.csrf().disable();
}
7. 鉴权

步骤1:在配置类上添加注解配置

@EnableWebSecurity
//@EnableGlobalMethodSecurity(jsr250Enabled = true)  //开启Security注解鉴权
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)  //sprintSecurity自带 可以支持Spring EL表达式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}

步骤2:在控制器方法上使用注解,表示必须拥有该注解标识的权限才能访问

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/userList")
    //一旦使用此注解,表示请求该方法的用户权限集里必须该权限标识符
    //@RolesAllowed("ROLE_teacher:list")  //访问到数据库表中的权限标识符必须以ROLE_开头,注解上的ROLE_可以省略
    //@Secured("ROLE_teacher:list") //访问到数据库表中的权限标识符必须以ROLE_开头 注解上的ROLE_不能省略
    //@PreAuthorize("hasAnyAuthority('teacher:list')") //使用hashAnyAuthority EL表达式,可以指定权限标识,不要求使用ROL_ 开头
    //@PreAuthorize("hasAnyRole('ROLE_teacher:list')") //使用hashAnyROLE EL表达式,可以指定权限标识,要求使用ROL_ 开头 ,数据库表中的权限标识也必须以ROLE开头
    @PreAuthorize("hasRole('ROLE_teacher:list')")
    public List<User> queryUserList(){
        return userService.list(null);
    }
}

步骤3:权限不足的处理方案

/**
 * 权限不足的处理
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        ResponseResult<Void> result = ResponseResult.error(ResultCode.NO_PERMISSION);
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out= response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
        out.flush();
        out.close();
    }
}

步骤4:配置类

http.authorizeRequests().antMatchers("/login", "/login.html")
    .permitAll().anyRequest().authenticated()
    .and().
    // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
    formLogin().loginPage("/login.html").loginProcessingUrl("/login")
    //登录表单form中密码输入框input的name名,不修改的话默认是password
    .usernameParameter("username").passwordParameter("password")
    //登录认证成功后默认转跳的路径
    //.defaultSuccessUrl("/home")
    // 前后端分离认证成功的处理器 -输出json
    .successHandler(myAuthenticationSuccessHandler)
    // 前后端分离认证失败的处理器 -输出json
    .failureHandler(myAuthenticationFailureHandler)
    .and()
    // 前后端分离处理未登录请求
    .exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
    // 前后端分离处理权限不足的请求
    .accessDeniedHandler(myAccessDeniedHandler);
//.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();
8. 整合JWT

步骤1:添加依赖jar

<!--用于生成JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.1</version>
</dependency>

步骤2:编写工具类JwtTokenUtil并测试

public class JwtTokenUtil {
    /**
     * 过期时间50分钟
     */
    private static final long EXPIRE_TIME = 5 * 60 * 10000;
    /**
     * jwt 密钥
     */
    private static final String SECRET = "woniuxy";

    /*
       生成签名  50分钟过期
     */
    public static String createSign(String userName) throws Exception {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            return JWT.create()
                    // 将 user id 保存到 token 里面
                    .withAudience(userName)
                    // 50分钟后token过期
                    .withExpiresAt(date)
                    //.withClaim()
                    //.withSubject(userName)
                    // token 的密钥
                    .sign(algorithm);
        }catch(Exception ex){
            ex.printStackTrace();
            throw new Exception("签名错误");
        }
    }

    /**
     * 根据token获取username
     * @param token
     * @return
     */
    public static String getUserId(String token) {
        try {
            String userId = JWT.decode(token).getAudience().get(0);
            return userId;
        } catch (JWTDecodeException e) {
            throw new JWTDecodeException("生成的token 异常");
        }
    }

    /**
     * 校验token 是否有效
     * @param token
     * @return
     */
    public static boolean checkSign(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    // .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (JWTVerificationException exception) {
            throw new RuntimeException("token 无效,请重新获取");
        }
    }

    public static void main(String[] args) throws Exception {
        //测试生成Token串
        String strToken = JwtTokenUtil.createSign("zhangsan");
        System.out.println(strToken);

        //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo
        //验证 token是否有效
        boolean isValid = JwtTokenUtil.checkSign("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
        System.out.println(isValid);

        //从给定的token串获取用户信息
        String username = JwtTokenUtil.getUserId("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
        System.out.println(username);
    }
}

步骤3:自定义认证成功的处理器Handler,用于登录认证成功后生成token并返回

**
 * 自定义认证成功的处理器Handler
 */
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        try {
            //获取当前登录认证成功的用户名
            String username = request.getParameter("username");
            String strToken = JwtTokenUtil.createSign(username);

            //通过响应的json返回客户端
            ResponseResult<String> result = new ResponseResult<>(strToken,"OK",200);
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            //将对象转json输出
            out.write(new ObjectMapper().writeValueAsString(result)); 
            out.flush();
            out.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

携带Token发送请求,要想使FilterSecurityInterceptor过滤器放行:

1. Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且应该是已认证状态。
2. Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。

实现思路:

关键在于:在 FilterSecurityInterceptor 之前 要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。

基于上述思路,

步骤4:我们要自定义实现一个 Filter :

/**
 * 如果当前请求没有携带 jwt-token , 啥事不干,直接放行。将当前请求 "漏给" 后面的 UsernamePasswordFilter 和 AnnonymouseFilter
 * 如果当前请求中有携带 jwt-token ,
 *   1. 校验 jwt-token 的合法性,看它是否是请求发起方伪造的,或者是是否已过期,等等。( jwt-token 的合法性校验)。
 *   2. 从 jwt-token 中获得当前的用户名,从 MySQL/Redis 查询这个人所具有的所有的权限。
 *   3. 把这个 用户名+权限集合 塞进 AuthenticationToken 中,再将 AuthenticationToken 放到上下文中(因为,SecurityInterceptor 要用它)。
 */
@Slf4j
@Component
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        // 不是认证请求--获取请求中的头部的token串
        String strToken = request.getHeader("strToken");

        if (StringUtils.isEmpty(strToken) || "null".equals(strToken)) {
            log.info("当前请求没有携带 jwt-token 。放行,漏给后面的 Annony...Fitler 。");
            filterChain.doFilter(request, response);
            return;
        }


        /* 不是空 */

        // 1. jwt-token 的合法性校验
        try {
            JwtTokenUtil.checkSign(strToken);
        } catch (RuntimeException ex) {
            log.info("当前请求有 jwt-token ,但是有问题(可能是伪造/瞎编的),放行,漏给后面的 Annony...Filter 。");
            filterChain.doFilter(request, response);
            return;
        }

        // 2. 合法性没问题,根据 jwt-token 中 "藏" 的用户名来查权限。
        String username = JwtTokenUtil.getUserId(strToken);
        // 查询数据库获取用户的权限集
        List<String> percodes = userMapper.getPerCodesByPerm(username);
        List<GrantedAuthority> authorities = new ArrayList<>();
        percodes.forEach(percode -> {
            authorities.add(new SimpleGrantedAuthority(percode));
        });

        // 3. 将当前用户所具有的所有的权限塞进 AuthenticationToken
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
                = new UsernamePasswordAuthenticationToken(username, "", authorities);

        // 4. 将 AuthentiationToken 存入 securityContext ,给最后的 SecurityInterceptor 用。
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

        // 5. 放行
        filterChain.doFilter(request, response);
    }
}

步骤5:然后将过滤器插入到FilterChainPrxoy代理的过滤器链中的UsernamePasswordAuthencationFilter前面

//将自定义的JwtTokenAuthenticationFilter插入到过滤器链中的指定的过滤器前面
             http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
9. 注销成功处理方案

步骤1:自定义注销成功的处理器MyLogoutSuccessHandler实现LogoutSuccessHandler接口

//注销成功的处理器
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        String headerToken = request.getHeader("strToken");
        System.out.println("logout header Token:"+headerToken);
        if(!StringUtils.isEmpty(headerToken)){ //如果token不是空
            SecurityContextHolder.clearContext(); //清空上下文 用户名与权限集UsernamePasswordAuthenticationToken
            ResponseResult<String> result = new ResponseResult<>("","注销成功",200);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(result));
        }else{
            ResponseResult<Void> result = ResponseResult.error(ResultCode.TOKEN_IS_NULL);
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out= response.getWriter();
            out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
            out.flush();
            out.close();
        }
    }
}

步骤2:在WebSecurityConfig配置类中 配置注销成功处理器

 // 前后端分离处理注销成功操作
 .and().logout().logoutSuccessHandler(myLogoutSuccessHandler);

//关闭session最严格的策略 -JWT认证的情况下,不需要security会话参与
 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值