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);