对于权限的管理,在企业应用程序的开发中,是必不可少的功能,但是能够灵活且强大的权限控制又不是一件容易的事情,所以在自己学习编写权限控制体系的基础上也接触一下成熟的框架,Spring 的全家桶系列 Spring Security 就进入了我们的视线。
Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
而认证和授权也是 Spring Security 作为安全框架的核心功能。
我们通过一个简单的入门案例来了解 Spring Security 。
我们已父子项目方式搭建,所有的依赖都在父项目中书写,子项目只需继承父项目即可。父项目中的写法:
org.springframework.boot
spring-boot-starter-parent
2.7.0
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
org.springframework.boot
spring-boot-starter-security
书写一个普通的 controller 类
@RestController
@RequestMapping("/guanwei")
public class GuanweiController {
@GetMapping
public String execute() {
return "This is GuanweiController`execute!";
}
}
@SpringBootApplication
public class Security01Application {
public static void main(String[] args) {
SpringApplication.run(Security01Application.class, args);
}
}
当我们还按照过去的方式访问 http://localhost:8081/guanwei 路径时,发现弹出了登录框,这个是 Spring Security 自带的登录页面,要求我们必须登录后才能访问系统资源。
并且在项目启动后,在控制台能看到初始生成的密码。
使用 Spring Security 后访问系统任意资源时,就会跳转到默认登录页面,默认账号是 user ,登录成功后,才能访问才能对目标接口进行访问。以后只要不关闭浏览器或者服务器,都可以直接访问。也可以手动登出,路径是:logout 。
我们刚才通过一个简单的案例,了解了 Spring Security 的基本概念。也发现 Spring Security 会在服务器启动时随机生成密码,那么有的童鞋就会想到,能不能自己去定义这个密码,甚至于使用数据库来校验用户登陆。
这种方式其实很简单,只需要在配置文件 (application.yml) 中设置账号密码就行。
spring:
security:
user:
name: root
password: guanwei
还可以在配置类中进行设置,注释上边的写法,在 config 包下创建一个配置类 SecurityConfig 来配置账号密码信息。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String pass = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("guanwei").password(pass).roles("admin");
}
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
BCryptPasswordEncoder 类是 Spring Security 中的一个加密方法类。BCryptPasswordEncoder 方法采用了 SHA-256+随机盐+密钥对密码进行加密。 SHA 是一种安全 Hash 函数(SHA),是使用最广泛的Hash函数。
加密算法与 hash 算法的区别:
- 加密算法是可逆的,加密算法的基本过程是对原来为明文的数据按某种算法进行处理,使其成为不可读的一段代码为“密文”,但在用相应的密钥进行操作之后就可以得到原来的内容 。
- hash 算法是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。
前面两种都是写死的,可能对于固定的超级管理员可以用,我们真实的项目场景肯定都是数据库里面的,肯定需要自定义查询,我们先把SecurityConfig 注释了,重新创建一个类 SpringSecurityConfig 来从数据库中判断账号密码。
引入数据库相关依赖
mysql
mysql-connector-java
runtime
com.baomidou
mybatis-plus-boot-starter
3.5.1
com.alibaba
druid-spring-boot-starter
1.2.9
书写 bean 包的类
@Data
public class Users implements Serializable {
@TableId(value = "id",type = IdType.AUTO)
private Integer id;
private String userName;
private String passWord;
private String nickName;
}
书写 mapper 包的类
@Mapper
public interface UsersMapper extends BaseMapper {
}
书写一个 service 类继承 UserDetailsService
@Service
public class LoginUsersServiceImpl implements UserDetailsService {
@Resource
private UsersMapper mapper;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper query = new QueryWrapper<>();
query.eq("username", username);
Users users = mapper.selectOne(query);
// 如果没有找到用户名 抛出异常 失败
if (users == null) {
System.out.println("账号或者密码错误");
throw new UsernameNotFoundException("账号或者密码错误");
}
// 这里是加载用户权限,这里先模拟个 admin 权限,更详细的在后边会说到
List auths = AuthorityUtils.createAuthorityList("admin");
return new User(username, new BCryptPasswordEncoder().encode(users.getPassWord()), auths);
}
}
书写 SpringSecurityConfig 配置类
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
}
这时候继续访问刚才的页面,输入数据库的账号和密码就行了。
我们刚才一直使用 security 自带的登陆页面,但是在实际使用中我们更多的是使用我们自己书写的登陆页面,要想设置其实很简单,如下几步就行。
创建登录页面
在配置类中设置登录页面
// 自定义登录页面
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义登陆页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登陆访问路径 这里同 action
.defaultSuccessUrl("/index.html").permitAll() //登陆成功之后,跳转路径
.and().authorizeRequests()
.antMatchers( "/login.html", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated() //所有请求都可以访问
.and().csrf().disable(); //关闭csrf
}
在项目中,有很多接口是针对不同角色权限的,如果角色是超级管理员,就拥有访问所有权限的能力,如果不是超级管理员,访问其他接口是不能允许的。 有些用户具有部分权限,就可以访问这些权限所能访问的内容,如果要实现这种效果,我们就需要授权管理。
hasAuthority()方法:如果当前的主体具有指定的权限,则返回 true,否则返回 false。
配置类
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义登陆页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登陆访问路径 这里同 action
.defaultSuccessUrl("/index.html").permitAll() //登陆成功之后,跳转路径
.and().authorizeRequests()
.antMatchers( "/login.html", "/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
.antMatchers("/index.html").hasAuthority("guanwei") // 设置当前登录用户访问index.html页面时需要guanwei权限
// 设置当前登录用户访问1.html页面时需要manager1、manager2、manager3或manager4权限
.antMatchers("/1.html").hasAnyAuthority("manager1","manager2","manager3","manager4")
.anyRequest().authenticated() //所有请求都可以访问
.and().csrf().disable(); //关闭csrf
}
这里我们可以看到访问 index.html 页面需要 guanwei 权限,而访问1.html 只要有 manager1、manager2、manager3 或 manager4 权限之一就行。
业务类
还是使用刚才的权限 guanwei,然后看看运行结果。
运行结果
访问 index.html 页面
访问 1.html 页面
我们更换权限为 manager1 来试一试
这里就可以直接访问了。
配置类
业务类
运行结果
当没有权限的时候访问会报403,我们可以自定义一个页面。
创建一个 403.html 页面
403
你没有相应的权限访问这个页面!
配置类中设置
运行结果
SpringSecurity 的注销功能很简单,只需要一个超链接地址为 /logout 就行。
Title
我是注销页面!
注销
通过刚才的案例我们了解到了 SpringSecurity 认证授权的效果。可是刚才通过的是表单方式进行提交,在其后的项目中我们可能更多的使用前后端分离效果,那么我们就要返回给前端对应的消息来告诉前端认证授权的情况。
前后端分离项目,需要后端返回消息来标注相应状态,这时候我们创建三个类来对返回的消息统一格式。
统一返回格式
/**
* @Author: Dailyblue
* @Description: 统一返回实体
* @Date Create in 2022/06/21 19:28
*/
@Data
public class JsonResult implements Serializable {
private Boolean success;
private Integer errorCode;
private String errorMsg;
private T data;
public JsonResult() {
}
public JsonResult(boolean success) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
}
public JsonResult(boolean success, ResultCode resultEnum) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
}
public JsonResult(boolean success, T data) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
this.data = data;
}
public JsonResult(boolean success, ResultCode resultEnum, T data) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
this.data = data;
}
}
返回状态码定义
/**
* @Author: Dailyblue
* @Description: 返回码定义
* 规定:
* #1表示成功
* #1001~1999 区间表示参数错误
* #2001~2999 区间表示用户错误
* #3001~3999 区间表示接口异常
* @Date Create in 2022/06/21 19:28
*/
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, "账号下线"),
/* 业务错误 */
NO_PERMISSION(3001, "没有权限");
private Integer code;
private String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
/**
* 根据code获取message
*
* @param code
* @return
*/
public static String getMessageByCode(Integer code) {
for (ResultCode ele : values()) {
if (ele.getCode().equals(code)) {
return ele.getMessage();
}
}
return null;
}
}
返回体构造工具
/**
* @Author: Dailyblue
* @Description: 返回体构造工具
* @Date Create in 2022/06/21 19:28
*/
public class ResultTool {
public static JsonResult success() {
return new JsonResult(true);
}
public static JsonResult success(T data) {
return new JsonResult(true, data);
}
public static JsonResult fail() {
return new JsonResult(false);
}
public static JsonResult fail(ResultCode resultEnum) {
return new JsonResult(false, resultEnum);
}
}
当用户未登录时,会自动进入当前类的 commence 方法,我们在这个方法中返回 JSON 格式的错误信息。
@Component
public class NotLoginAuthentication implements AuthenticationEntryPoint {
// 当用户未登录时 访问资源会返回结果
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
当用户登录失败时(不论是账号未找到,密码错误还是权限问题),都会进入这个类的指定方式,我们在这个方法中返回 JSON 格式的错误信息。
@Component
public class FailureAuthenticationHandler implements AuthenticationFailureHandler {
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException, ServletException {
//返回json数据
JsonResult result;
if (e instanceof AccountExpiredException) {
//账号过期
result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//密码错误
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (e instanceof DisabledException) {
//账号不可用
result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
} else if (e instanceof LockedException) {
//账号锁定
result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
} else if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
} else {
//其他错误
result = ResultTool.fail(ResultCode.COMMON_FAIL);
}
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到response中返回给前台
response.getWriter().write(JSON.toJSONString(result));
}
}
同样的道理,对正确的消息返回 JSON 格式信息。
@Component
public class SuccessAuthenticationHandler implements AuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//返回json数据
JsonResult result = ResultTool.success();
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到response中返回给前台
response.getWriter().write(JSON.toJSONString(result));
}
}
在配置文件中注册三个效果。
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.permitAll()
.successHandler(successHandler) //登录成功处理逻辑
.failureHandler(failureHandler) //登录失败处理逻辑
//异常处理(权限拒绝、登录失效等)
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
.and()
.authorizeRequests()
.antMatchers("/login.html", "/login").permitAll()
.antMatchers("/dailyblue/guanwei").hasAnyAuthority("admin")
.anyRequest().authenticated() //所有请求都可以访问
.and().csrf().disable(); //关闭csrf
}
登录成功的情况
登录失败的情况
没有登录就访问其他资源情况
刚才的例子中,我们可以通过几个 Handler 操作未登录时、登录失败和登录成功情况。那么前后端分离情况下,如何保存用户登录状态呢?下边我们通过 Security 的几个过滤器来实现这个功能。
我们这里通过 JWT 令牌方式来验证用户登录。JWT 登录详情可以查看另一篇博客。这里创建 JwtConfig 类。
package com.dailyblue.config;
import com.dailyblue.bean.Users;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* @author dailyblue
* @since 2022/6/23
*/
@Component
public class JwtConfig {
//常量
public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
public static final String APP_SECRET = "1234"; //秘钥,加盐
// @param id 当前用户ID
// @param issuer 该JWT的签发者,是否使用是可选的
// @param subject 该JWT所面向的用户,是否使用是可选的
// @param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
// @param audience 接收该JWT的一方,是否使用是可选的
// 生成json token字符串的方法
public static String getJwtToken(Users user) {
String jwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT") //头部信息
.setHeaderParam("alg", "HS256") //头部信息
//下面这部分是payload部分
// 设置默认标签
.setSubject("dailyblue") //设置jwt所面向的用户
.setIssuedAt(new Date()) //设置签证生效的时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) //设置签证失效的时间
//自定义的信息,这里存储id和姓名信息
.claim("id", user.getId()) //设置token主体部分 ,存储用户信息
.claim("name", user.getUserName())
.claim("nickName", user.getNickName())
//下面是第三部分
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
// 生成的字符串就是jwt信息,这个通常要返回出去
return jwtToken;
}
/**
* 判断token是否存在与有效
* 直接判断字符串形式的jwt字符串
*
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* 因为通常jwt都是在请求头中携带,此方法传入的参数是请求
*
* @param request
* @return
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");//注意名字必须为token才能获取到jwt
if (StringUtils.isEmpty(jwtToken)) return false;
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token字符串获取会员id
* 这个方法也直接从http的请求中获取id的
*
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if (StringUtils.isEmpty(jwtToken)) return "";
Jws claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String) claims.get("id");
}
/**
* 解析JWT
* @param jwt
* @return
*/
public static Claims parseJWT(String jwt) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
return claims;
}
}
SecurityUser 类,这个类描述用户信息和它的权限信息。
package com.dailyblue.bean;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* UserDetailService 使用该类,该类必须是 UserDetails 的子类
*/
@Data
public class SecurityUser implements UserDetails {
// 登录用户的基本信息
private Users user;
//当前权限
private List permissionValueList;
public SecurityUser() {
}
public SecurityUser(Users user) {
if (user != null) {
this.user = user;
}
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
permissionValueList.forEach(permission ->{
if(!StringUtils.isEmpty(permission)){
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
});
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;
}
}
UsersService 类,业务类,负责调用 Mapper 的方法
package com.dailyblue.service;
import com.dailyblue.bean.SecurityUser;
import com.dailyblue.bean.Users;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
@Slf4j
public class UsersService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("进入了UsersService的loadUserByUsername方法,接受传递参数:{}", username);
Users user = null;
// 这里没有连接数据库 模拟数据
if ("guanwei".equals(username)) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
user = new Users(1, "guanwei", passwordEncoder.encode("123456"), "关为");
}
// 判断用户是否存在
if (user == null) {
throw new UsernameNotFoundException("账户信息不存在!");
}
// 这里没有连接数据库 模拟数据
List admin = Arrays.asList("manager1", "manager2");
SecurityUser securityUser = new SecurityUser(user);
securityUser.setPermissionValueList(admin);
return securityUser;
}
}
这个是一个 Filter ,不需要 Spring 来注入
后两个如果书写了,上一个案例中的那两个 Handler 就可以不书写了。
package com.dailyblue.filter;
import com.alibaba.fastjson.JSONArray;
import com.dailyblue.bean.SecurityUser;
import com.dailyblue.bean.Users;
import com.dailyblue.config.JwtConfig;
import com.dailyblue.util.JsonResult;
import com.dailyblue.util.ResultCode;
import com.dailyblue.util.ResultTool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private JwtConfig jwtConfig;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(JwtConfig jwtConfig, AuthenticationManager authenticationManager) {
this.jwtConfig = jwtConfig;
this.authenticationManager = authenticationManager;
// 关闭登录只允许 post
this.setPostOnly(false);
// 设置登陆路径,并且post请求
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login", "POST"));
}
// 获取登录页面传递过来的账号和密码
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("用户开始登录...");
String userName = request.getParameter("userName");
String password = request.getParameter("password");
log.info("账号:{},密码:{}", userName, password);
// 登录接口 /user/login 调用请求时触发
// UsernamePasswordAuthenticationToken 封装登录时传递来的数据信息
// 交给 AuthenticationManager 进行登录认证校验
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
}
// 配置成功登录
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("用户登录成功!");
// 认证成功之后,获取认证后的用户基本信息
SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
log.info("用户信息是:{}", securityUser);
Users user = securityUser.getUser();
String token = JwtConfig.getJwtToken(user);
log.info("用户token是:{}", token);
// token信息存于redis、数据库、缓存等
//返回json数据
JsonResult result = ResultTool.success();
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到response中返回给前台
response.getWriter().write(JSONArray.toJSONString(result));
}
// 配置失败登录
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.info("用户登录失败!");
//返回json数据
JsonResult result;
if (e instanceof AccountExpiredException) {
//账号过期
result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//密码错误
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (e instanceof DisabledException) {
//账号不可用
result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
} else if (e instanceof LockedException) {
//账号锁定
result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
} else if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
} else {
//其他错误
result = ResultTool.fail(ResultCode.COMMON_FAIL);
}
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到response中返回给前台
response.getWriter().write(JSONArray.toJSONString(result));
}
}
这个过滤器会在每次请求(不需要触发的可以在配置文件中设置)时去触发,主要作用是验证用户是否登录。
package com.dailyblue.filter;
import com.dailyblue.config.JwtConfig;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@Slf4j
public class TokenAuthFilter extends BasicAuthenticationFilter {
private JwtConfig jwtConfig;
public TokenAuthFilter(AuthenticationManager authenticationManager, JwtConfig jwtConfig) {
super(authenticationManager);
this.jwtConfig = jwtConfig;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("开始校验用户是否登录");
String token = request.getHeader("token");
log.info("token:{}", token);
if (token != null) {
// 本次模拟 这里没有校验Redis
Claims claims = JwtConfig.parseJWT(token);
String nickName = claims.get("nickName").toString();
log.info("获取的昵称是:{}", nickName);
// 本次模拟 没有连接数据库
List permissionValueList = Arrays.asList("manager1", "manager2");
Collection authority = new ArrayList<>();
for (String permissionValue : permissionValueList) {
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(nickName, token, authority);
// 有权限,则放入权限上下文中
SecurityContextHolder.getContext().setAuthentication(upat);
}
chain.doFilter(request, response);
}
}
package com.dailyblue.config;
import com.dailyblue.filter.TokenAuthFilter;
import com.dailyblue.filter.TokenLoginFilter;
import com.dailyblue.handler.FailureAccessDeniedHandler;
import com.dailyblue.handler.NotLoginAuthentication;
import com.dailyblue.service.UsersService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 方法增加权限
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtConfig jwtConfig;
@Resource
private UsersService usersService;
@Resource
private NotLoginAuthentication notLoginAuthentication;
@Resource
private FailureAccessDeniedHandler failureAccessDeniedHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
log.info("进入SecurityConfig的configure方法");
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
auth.userDetailsService(usersService).passwordEncoder(passwordEncoder);
}
/**
* 配置访问过滤
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(notLoginAuthentication) // 未登录 handler
.accessDeniedHandler(failureAccessDeniedHandler) // 无权限
.and().csrf().disable() // 关闭 csrf 跨域请求
.cors().and() // security允许跨域
.formLogin()
.loginProcessingUrl("/user/login") // 设定登录请求接口
.usernameParameter("userName")
.passwordParameter("password")
.permitAll()
.and()
.authorizeRequests() // 请求设置
.antMatchers("/guanwei").permitAll() // 配置不需要认证的接口
.anyRequest().authenticated() // 任何请求都需要认证
.and()
.addFilter(new TokenLoginFilter(jwtConfig, authenticationManager())) // 认证交给 自定义 TokenLoginFilter 实现
.addFilter(new TokenAuthFilter(authenticationManager(), jwtConfig))
.httpBasic();
}
@Bean
PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
添加方法级别的角色控制,可以通过注解的方式来完成,在需要具有角色或权限方法的上引入 @PreAuthorize 注解。
这里为了方便期间,没有引入数据库,账号和密码都是写死的。
@RestController
@RequestMapping("/guan")
public class GuanController {
@GetMapping("/a")
@PreAuthorize("hasAnyAuthority('admin')")
public String a() {
return "Hello,world!";
}
@GetMapping("/b")
@PreAuthorize("hasAnyAuthority('admin','guan')")
public String b() {
return "This is method`b!";
}
@GetMapping("/c")
@PreAuthorize("hasAnyAuthority('team')")
public String c() {
return "This is method`c!";
}
}
访问 a 方法
访问 b 方法
SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,这里我们可以看看入门案例中的过滤器。
1 . org.springframework.security.web.context.SecurityContextPersistenceFilter
首当其冲的一个过滤器,作用之重要,自不必多言。
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个
SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
SecurityContext中存储了当前用户的认证以及权限信息。
2 . org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
3 . org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4 . org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
如果不包含,则报错。起到防止csrf攻击的效果。
5. org.springframework.security.web.authentication.logout.LogoutFilter
匹配 URL为/logout的请求,实现用户退出,清除认证信息。
6 . org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
7 . org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
8 . org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
9 . org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
10 . org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
11 . org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12 . org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
13 . org.springframework.security.web.session.SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量
14 . org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
15 . org.springframework.security.web.access.intercept.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
/**
* spring security 核心配置文件
*/
@Configuration
public class BrowerSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired //自定义的安全元 数据源 实现FilterInvocationSecurityMetadataSource
private MyInvocationSecurityMetadataSourceService myInvocationSecurityMetadataSourceService;
@Autowired //自定义访问决策器
private MyAccessDecisionManager myAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* from表单登录设置
*/
http.formLogin()
.loginPage("") //登录页面 /login
.passwordParameter("") //设置form表单中对应的name参数 默认为 password 下同
.usernameParameter("") //
.defaultSuccessUrl("") //认证成功后的跳转页面 默认跳转页面 可以设置是否总是默认 不是的话可以跳转与用户的target-url
.failureUrl("")
.failureForwardUrl("") //登录失败 转发 的url
.successForwardUrl("") //登录成功 转发 的url 与successHandler对应 即处理完后请求转发的url
.failureHandler(null) //自定义的认证失败 做什么处理
.successHandler(null) //自定义认证成功 后做的处理 ----- 例如 想记录用户信息判断用户状态等
.permitAll() //对于需要所有用户都可以访问的界面 或者url进行设置
.loginProcessingUrl("") //自定义处理认证的url 默认为 /login
.authenticationDetailsSource(null) //自定义身份验证的数据源 理解为查出数据库中的密码 和权限(可以不加) 然后再交给security
修改和替换配置 已经配置好的修改 例如下面修改 安全拦截器的安全数据源
.withObjectPostProcessor(new ObjectPostProcessor() {
public O postProcess(
O fsi) {
fsi.setPublishAuthorizationSuccess(true);
//修改成自定义的 安全元数据源 权限的源 !!!!!
fsi.setSecurityMetadataSource(myInvocationSecurityMetadataSourceService);
//修改成自定义的 访问决策器 自定义的
fsi.setAccessDecisionManager(myAccessDecisionManager);
//使用系统的
fsi.setAuthenticationManager(authenticationManager);
return fsi;
}
});
/**
* 请求认证管理
*/
http.authorizeRequests()
.antMatchers("url匹配路径").permitAll() //url匹配路径 permitAll 运行 全部访问 不用认证
.accessDecisionManager(null) //访问决策器
.filterSecurityInterceptorOncePerRequest(true) //过滤每个请求一次的安全拦截器 ???
.anyRequest().authenticated() //其他的请求 需要认证,
.antMatchers("/admin/**").hasRole("ADMIN") //url匹配路径 具有怎样的角色
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") //url匹配路径 具有怎样的角色 或者是权限
;
/**
* anonymous
*
* 匿名访问时 存在默认 用户名 annonymousUser
*/
http.anonymous().disable().csrf().disable(); //禁止匿名 关闭csrf
/**
* 登出操作管理
*/
http.logout() //登出处理
.logoutUrl("/my/logout")
.logoutSuccessUrl("/my/index")
.logoutSuccessHandler(null)
.invalidateHttpSession(true)
.addLogoutHandler(null)
.deleteCookies("cookieNamesToClear")
;
/**
* session 会话管理
*/
http.sessionManagement() //session管理
.maximumSessions(2) //最大session 数量 --用户
.maxSessionsPreventsLogin(false) //超过最大sessin数量后时候阻止登录
.expiredUrl("/") //会话失效后跳转的url
.expiredSessionStrategy(null) //自定义session 过期错略
.sessionRegistry(null) //自定义的session 注册 表
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**
* 基础的配置
*/
auth
/**
* 认证 时触发的事件
*/
.authenticationEventPublisher(null)
/**
* 用户细节服务
*
* 认证管理器数据的来源 吧 用户身份凭证信息和 权限信息
*/
.userDetailsService(null)
/**
* 密码编辑器 对密码进行加密
*/
.passwordEncoder(null)
;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
/**
* 不进行拦截的mvc
*/
web.ignoring().mvcMatchers();
/**
* 添加自定义的 安全过滤器
*/
web.addSecurityFilterChainBuilder(null);
}
}