SpringSecurity
一、SpringSecurity概念
1、Spring Security是基于J2EE开发的企业应用软件提供了全面的安全服务,是针对Spring项目的安全框架,也是SpringBoot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.1</version>
</dependency>
二、编写基础配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
- @EnableWebSecurity注解 ,以启用Spring Security的Web安全支持,并提供Spring MVC集成。它还继承了WebSecurityConfigurerAdapter,并覆盖了一些方法来设置Web安全配置的一些细节。
- @EnableGlobalMethodSecurity(prePostEnabled = true)注解,当我们想要开启spring方法级安全时,只需要在任何 @Configuration实例上使@EnableGlobalMethodSecurity 注解就能达到此目的。同时这个注解为我们提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制来实现同一种功能
三、自定义用户认证(基于数据库的认证)
3.1、认证概念
Spring Security提供了很多过滤器,它们拦截Servlet请求,并将这些请求转交给认证处理过滤器和访问决策过滤器进行处理,并强制安全性认证用户身份和用户权限以达到保护WEB资源的目的,Spring Security安全机制包括两个主要的操作,认证和验证,验证也可以称为权限控制,
这是Spring Security两个主要的方向,认证是为用户建立一个他所声明的主体的过程,这个主体一般是指用户设备或可以在系统中执行行动的其他系统,验证指用户能否在应用中执行某个操作,在到达授权判断之前身份的主体已经由身份认证过程建立了
3.2、基于内存认证
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//认证:可以基于数据库认证或者内存认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("qianqian").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}
3.3、基于数据库认证 - -自定义认证
3.3.1、获取表单数据
- 值得注意的是,
SpringSecurity原生的UsernamePasswordAuthenticationFilter仅支持账户和密码的表单参数校验
,对于前端传来的如验证码等其他表单元素,并无法进行验证
- 虽然UsernamePasswordAuthenticationFilter仅支持账户和密码校验,但是SpringSecurity为我们提供了解决办法,
WebAuthenticationDetails: 该类提供了获取用户登录时携带的额外信息的功能,默认提供了 remoteAddress 与 sessionId 信息。我们还可以添加自己所需要的信息,比如验证码
- 但是转念一想,如果你只是继承了WebAuthenticationDetails,并且添加自己所需要的属性!但是好像并没有真正的把它告诉给SpringSecurity,说“喂,我把你原生的属性替换了!”,所以下一步我们需要把SpringSecurity原生的WebAuthenticationDetails替换成自己实现的WebAuthenticationDetails,这个时候就需要去实现
AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails>
- 最后最最最最最重要的事一定要在SpringSecurity的配置类中让他生效
- 简单的画了一张流程图,加深理解
- WebAuthenticationDetails
public class UserWebAuthenticationDetails extends WebAuthenticationDetails {
private static Logger log = LoggerFactory.getLogger(UserWebAuthenticationDetails.class);
private static final long serialVersionUID = 6975601077710753878L;
private final String code;
/**
* @param request that the authentication request was received from
*/
public UserWebAuthenticationDetails(HttpServletRequest request) {
super(request);
code=request.getParameter("code");
log.info("进入到了获取验证码的过滤器-->WebAuthenticationDetails,获取到的验证码-->"+code);
}
public String getCode() {
return code;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append("; code: ").append(this.getCode());
return sb.toString();
}
}
- AuthenticationDetailsSource
@Component("authenticationDetailsSource")
public class UserAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
private static Logger log = LoggerFactory.getLogger(UserAuthenticationDetailsSource.class);
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
log.info("开始替换SpringSecurity原生的WebAuthenticationDetails");
return new UserWebAuthenticationDetails(request);
}
}
- SecurityConfig
这里只贴了部分代码,太多的话会导致不知道具体在干嘛,还有我自己在写的时候,这个替换规则一定要写在.formLogin()这个开启表单验证的后面
/*
获取登录表单额外参数
*/
@Autowired
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
.and()
.formLogin()
.loginProcessingUrl("/admin/user/login").permitAll()
.successHandler(getLoginSuccessHandler())
.failureHandler(getLoginFailHandler())
.authenticationDetailsSource(authenticationDetailsSource)
- 至此,获取前端传来的表单元素就结束了,但是是真的结束了么?敲代码的心得就是,当我启动的时候,理想状态是他没有报错!但是现实却如此骨感,他怎么会不报错呀!长时间下来,他如果不报错我都觉得不正常!但是话说回来,这确实还有一个比较大的坑,这坑又大又圆,如果只是从后端考虑的话,确实难搞。
- 我这个项目是前后端分离,前端是用vue写的,学过的同学应该知道,vue默认交互数据的方式是
json
,但是呢,这个SpringSecurity呢就离了个大谱,他默认支持的方式x-www-form-urlencoded
方式的!所以我就在这被坑了很长一段时间,找到问题之后就好解决了,前端用qs转换一下数据格式就OK了,转换数据之后如果按正常流程的话,vue会自动识别数据的格式,他还是用自动转换为json数据格式,所以咱们发送请求就使用传统的请求发送就OK了,大功告成
//登录
login(data){
return request.post(`/api/admin/user/login?`+data)
},
//发送登录请求
user.login(qs.stringify(this.loginForm, {arrayFormat: 'indices', allowDots: true}))
//发送的请求格式
http://localhost:8080/api/admin/user/login?username=admin&password=admin&code=wjud
3.3.2、自定义认证流程
- 我们上边说到了
UsernamePasswordAuthenticationFilter
这个过滤器并不足以支持完成我们校验功能,所以我们需要自己去实现一个AuthenticationProvider
。 - spring security5+后密码策略变更了。必须使用 PasswordEncoder 方式,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,新版本中需要指定密码编码格式,我们需要去实现
PasswordEncoder
,密码加密规则有很多种,可以使用SpringSecurity原生的,也可以使用自己封装好的工具类,比如MD5等等。 - 所谓基于数据库的校验,那肯定得跟数据库打交道啊,咱在这一顿操作下来,发现没有一丁点数据库的事呀,介是嘛呀!所以到了这一步,没有学过的同学也该想到,SpringSecurity应该会提供一个接口让你去实现自己的查询逻辑,那么他就来了,我们需要实现
UserDetailsService
这个接口!这个接口内部默认需要去实现的方法Public UserDetails loadUserByUsername(String username)
,在这个方法内部我们就可以实现自己的查询逻辑了!但仔细一看这个方法的返回值,介是嘛呀!没见过呀,赶紧点进去看一看,哦,原来是一个接口呀!那就好说了,实现它!讲道理还是要弄清楚的,这个接口里边定义几个很重要的属性!百度一下,哦,原来如此!UserDetails ,该接口是提供用户信息的核心接口
。该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象Authentication中去。
- 那我们实现这个接口,完成我们自己的的信息存储
- 再来一个流程图,我们完成这一阶段的工作
- AuthenticationProvider
@Component
public class LocalAuthenticationProvider implements AuthenticationProvider {
private static Logger log = LoggerFactory.getLogger(LocalAuthenticationProvider.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DefaultPasswordEncoderUtil defaultPasswordEncoderUtil;
@Autowired
private RedisUtils redisUtils;
@Override
public Authentication authenticate(Authentication authentication) throws BadCredentialsException {
log.info("进入自定义验证规则");
String username = authentication.getName();
String password = (String)authentication.getCredentials();
UserWebAuthenticationDetails details = (UserWebAuthenticationDetails) authentication.getDetails();
// 获取Request, 获取其他参数信息
String code = details.getCode();
//验证码比较
String redisCode = (String)redisUtils.get("code");
if(redisCode.toString().equalsIgnoreCase(code)){
//去找自己实现UserDetails的实现类
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(defaultPasswordEncoderUtil.matches(password,userDetails.getPassword())){
//用户名密码校验成功
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}
log.info("用户名或密码不正确");
throw new BadCredentialsException("用户名或密码不正确!");
}
log.info("验证码不正确");
throw new BadCredentialsException("验证码不正确!");
}
/**
* supports函数用来指明该Provider是否适用于该类型的认证,如果不合适,则寻找另一个Provider进行验证处理
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
//和SpringSecurity
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
- PasswordEncoder
@Component
public class DefaultPasswordEncoderUtil implements PasswordEncoder {
private static Logger log = LoggerFactory.getLogger(DefaultPasswordEncoderUtil.class);
public DefaultPasswordEncoderUtil(){
this(-1);
}
public DefaultPasswordEncoderUtil(int strLength){
}
/**
* 进行MD5密码加密
* @param rawPassword
* @return
*/
@Override
public String encode(CharSequence rawPassword) {
return MD5Utils.encrypt(rawPassword.toString());
}
/**
* 进行密码比较
* @param rawPassword
* @param encodedPassword
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
log.info("DefaultPasswordEncoderUtil---->前端传入的密码rawPassword==="+rawPassword.toString());
log.info("DefaultPasswordEncoderUtil---->数据库中的密码encodedPassword==="+encodedPassword);
return encodedPassword.equals(MD5Utils.encrypt(rawPassword.toString()));
}
}
- UserDetailsService
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private static Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UserService userService;
@Autowired
private MenuService menuService;
/**
* 根据用户名查出用户详细信息
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("进入用户验证---->UserDetailsServiceImpl");
//根据用户名查询
ManUser user = userService.queryUser(username);
log.info("查到的用户数据==="+user.toString());
//判断用户信息
if(user == null){
log.info("用户不存在");
throw new UsernameNotFoundException("用户不存在");
}
//查询当前用户所关联的所有菜单
ManUserVO manUserVO = userService.queryUserRelationAllMenu(user);
if(manUserVO==null){
throw new UsernameNotFoundException("用户不存在");
}
List<ManMenu> menuList = manUserVO.getMenuList();
ArrayList<String> menuStringList = new ArrayList<>();
for (ManMenu manMenu : menuList) {
menuStringList.add(manMenu.toString());
}
SecurityUserVO securityUserVO = new SecurityUserVO();
securityUserVO.setCurrentUserInfo(user);
securityUserVO.setPermissionValueList(menuStringList);
log.info("验证数据"+securityUserVO.toString());
//校验账户状态属性是否正常,
return securityUserVO;
}
}
- UserDetails
@Data
public class SecurityUserVO implements UserDetails {
//当前登录用户
private transient ManUser currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUserVO() {
}
public SecurityUserVO(ManUser currentUserInfo) {
if (currentUserInfo!=null) {
this.currentUserInfo = currentUserInfo;
}
}
/**
* 获取角色权限
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (String permissionValue : permissionValueList) {
if (StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
/**
* 获取密码
* @return
*/
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
/**
* 获取用户名
* @return
*/
@Override
public String getUsername() {
return currentUserInfo.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;
}
}
- 我把我代码中的执行流程也贴一下,因为我都有在进行日志输出,所以可以直观的看到具体的执行流程
3.3.3、自定义认证成功或者认证失败处理器
- 前面的操作让我们对认证的流程有了一定的了解,但是认证还没有结束,我们需要自定义实现认证成功和认证失败的处理器。认证成功中的业务逻辑会涉及到动态权限的管理,所以也非常重要!
- 自定义认证成功处理器
AuthenticationSuccessHandler
,认证成功之后我们需要把用户信息还有后端生成的Token反馈给前端,这里说到Token的保存方式有很多,大家可以自由选择,我这里是把用户信息和token都放到了Redis里边 - 自定义认证失败处理器
AuthenticationFailureHandler
,失败就直接返回失败原因就好了 - 再来一张图,一步一步的梳理整个流程,辣么多的过滤器和处理器,着实头疼!
- 一定要记得在SecurityConfig中把相应的服务配置进去
- AuthenticationSuccessHandler
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private static Logger log = LoggerFactory.getLogger(LoginSuccessHandler.class);
private JwtUtil jwtUtil;
@Autowired
private RedisUtils redisUtils;
private static String signatureAlgorithmSecret="abcdefghijklmnopqrstuvwxyz123456789";
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//认证成功,得到认证成功之后的用户信息
log.info("自定义登录处理器认证成功");
log.info("当前的登录时间为:" + new Date().toString());
log.info("当前的登录IP为:" + request.getRemoteAddr());
SecurityUserVO user = (SecurityUserVO) authentication.getPrincipal();
//根据用户名生成token
PayloadDTO payload = jwtUtil.getPayload(user.getCurrentUserInfo());
// 生成token
String payloadJsonString = JSON.toJSONString(payload);
String token;
try {
token = JwtUtil.generateToken(payloadJsonString, signatureAlgorithmSecret);
//把用户名称和用户权限列表存放到redis中
redisUtils.set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//返回token
Map<String,Object> userInfoAndToken=new HashMap<>();
userInfoAndToken.put("userInfo",user.getCurrentUserInfo());
userInfoAndToken.put("token",token);
ResponseUtil.out(response, Result.result("000","login success",userInfoAndToken));
} catch (JOSEException e) {
e.printStackTrace();
}
}
}
- AuthenticationFailureHandler
public class LoginErrorHandler implements AuthenticationFailureHandler {
private static Logger log = LoggerFactory.getLogger(LoginErrorHandler.class);
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("自定义登录处理器认证失败");
ResponseUtil.out(response, Result.result("445","captcha error",null));
}
}
- SpringConfig
/**
* 自定义校验
*/
@Bean
public AuthenticationProvider authenticationProvider() {
LocalAuthenticationProvider authenticationProvider = new LocalAuthenticationProvider();
return authenticationProvider;
}
// 登录成功处理器
@Bean
public LoginSuccessHandler getLoginSuccessHandler() {
return new LoginSuccessHandler();
}
// 登录失败处理器
@Bean
public LoginErrorHandler getLoginFailHandler() {
return new LoginErrorHandler();
}
3.3.4、总结
- 至此,我们的认证流程就走完了,这里给大家详细的讲了每一步及每一步出现的过滤器的作用,相信大家有所收获,抓紧去试试吧!千里之行,始于足下。万行代码,始于New对象!加油哦!
四、动态授权及权限控制
- SpringSecurity提供了默认的权限控制功能,需要预先分配给用户特定的权限,并指定各项操作执行所要求的权限。用户请求执行某项操作时,SpringSecurity会先检查用户所拥有的权限是否符合执行该项操作所要求的权限,如果符合,才允许执行该项操作,否则拒绝执行该项操作。
- 再认证的流程中我们知道,在认证成功之后,后端会跟根据用户信息生成一个token,给前端返回,那么前端接收到这个token之后,就需要为每次请求都加上这个token,token可以放到请求头中,常见的请求头方式为‘
’Authorization : Basic token字符串
‘’ - 我们在拿到token之后,想一想为什么要使用token呢?为什么不使用session呢?这里不知道的小伙伴可以去查一下。下面这张图是传统的基于的token的权限控制,可以看到这里是没有使用SpringSecurity的。
- 我们现在讲基于SpringSecurity的。前边讲到认证通过之后,我们把用户信息和用户权限列表都存入了Redis中,那么现在我们就实现动态授权,即自定义授权。同学们现在应该有自己的想法了。前边认证我们需要去实现自定义认证器,作为授权我们也要去实现自定义授权器。我们要实现
BasicAuthenticationFilter
这个过滤器。仔细一看发现这个怎么这么熟悉呀,没错 BasicAuthentication不就是token的请求头么!这个过滤的作用就是处理HTTP请求中的BASIC authorization头部,把认证结果写入SecurityContextHolder。当一个HTTP请求中包含一个名字为Authorization的头部,并且其值格式是Basic xxx时, 该Filter会认为这是一个BASIC authorization头部
,这里边最重要的一个对象要来了,它贯穿整个SpringSecurity中,它就是SecurityContextHolder(Security安全上下文对象)
,作为程序猿,同学们听该听过特别多的什么什么上下文对象了,像什么Spring上下文对象,Application配置类上下文对象。那么这些上下文对象主要的作用是什么呢? - 我们先了解一下什么为上下文对象,
上下文即ServletContext,是一个全局的储存信息的空间,服务器启动,其就存在,服务器关闭,其才释放。所有用户共用一个ServletContext。所以,为了节省空间,提高效率,ServletContext中,要放必须的、重要的、所有用户需要共享的线程又是安全的一些信息。
,这么一看下来,这下子同学们心里有答案了吧,我们就是要在这个BasicAuthenticationFilter过滤器的实现类中把权限列表存入上下文对象! - 这一步至关重要,上边我们已经拿到了属于当前用户的权限列表,但是仅仅拿到就有用了么,细想,不就是执行了一次方法么?就这样子就可以进行授权了么?同学们发挥想象力,我们在设计数据表时是基于RBAC原则设计的,授权方式都是通过给角色授权在间接的给用户授权,我们在拿到权限列表时,是不是应该判断一下当前的请求URL即菜单的URL是不是属于当前角色呢?
在这里我们提到了两个问题,一是怎么拿到当前请求URL,二是怎么判断当前请求URL属不属于当前用户所关联的角色呢?
- 这里我们解决第一个问题,拿到当前请求的URL!这里SpringSecurity也为我们提供了接口
FilterInvocationSecurityMetadataSource
这个接口可以称为权限资源过滤器,实现动态的权限验证,它的主要责任就是当访问一个url时,返回这个url所需要的访问权限。这里我们就解决了第一个问题 - 开始解决第二个问题,也是最关键的!我们在上一步拿到需要的访问权限之后,理所应当现在需要进行对比,最终给他将没有权限的URL踢回去!现在就需要将存入的上下文中的权限列表跟我们这个URL所需要的权限列表进行对比,这里我们就需要使用到权限决策器
AccessDecisionManager
- 我们统一的将没有权限的返回
- 理所当然,肯定要再来一张图啦! 这里呢,我们后端的权限控制就结束了,下一步就开始前端根据后端返回的权限列表实现动态路由,实时渲染!
- BasicAuthenticationFilter
public class TokenAuthFilter extends BasicAuthenticationFilter {
private static Logger log = LoggerFactory.getLogger(TokenAuthFilter.class);
private JwtUtil jwtUtil;
private RedisUtils redisUtils;
public TokenAuthFilter(AuthenticationManager authenticationManager,JwtUtil jwtUtil, RedisUtils redisUtils) {
super(authenticationManager);
this.jwtUtil = jwtUtil;
this.redisUtils = redisUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("已认证成功,开始授权");
//获取当前认证成功用户权限信息
UsernamePasswordAuthenticationToken authRequest=getAuthentication(request);
if(authRequest != null){
log.info("authRequest==========="+authRequest.toString());
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request,response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
String token = request.getHeader("Authorization");
if(token != null){
log.info("token========"+token);
//获取用户名
try {
String username = JwtUtil.getUsername();
log.info(username);
//获取该用户所拥有的菜单信息
List<String> permissionValueList = (List<String>) redisUtils.get(username);
if(permissionValueList.isEmpty()){
return null;
}
Collection<GrantedAuthority> authorities=new ArrayList<>();
for (String permissionValue : permissionValueList) {
SimpleGrantedAuthority auth=new SimpleGrantedAuthority(permissionValue);
authorities.add(auth);
}
return new UsernamePasswordAuthenticationToken(username,token,authorities);
} catch (ParseException e) {
e.printStackTrace();
} catch (JwtSignatureVerifyException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
}
return null;
}
}
- FilterInvocationSecurityMetadataSource
@Component
public class UserFileterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private static Logger log = LoggerFactory.getLogger(UserFileterInvocationSecurityMetadataSource.class);
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private MenuService menuService;
/**
* 返回本次访问需要的权限列表
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
log.info("进入了安全数据源");
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//去数据库查询资源
List<ManMenu> allMenu = menuService.queryAllMenu();
for (ManMenu menu : allMenu) {
if (antPathMatcher.match(menu.getOpenAddress(), requestUrl)) {
List<ManRole> manRoles = menuService.queryAllMenuRelationRole(menu.getMenuId());
// List<Role> roles = menu.getRoles();
int size = manRoles.size();
String[] values = new String[size];
for (int i = 0; i < size; i++) {
values[i] = manRoles.get(i).getRoleName();
}
log.info("当前访问路径是{},这个url所需要的访问权限是{}", requestUrl,values);
return SecurityConfig.createList(values);
}
}
/**
* @Author: Galen
* @Description: 如果本方法返回null的话,意味着当前这个请求不需要任何角色就能访问
* 此处做逻辑控制,如果没有匹配上的,返回一个默认具体权限,防止漏缺资源配置
**/
log.info("当前访问路径是{},这个url所需要的访问权限是{}", requestUrl, "ROLE_LOGIN");
return SecurityConfig.createList("ROLE_LOGIN");
}
/**
* 此处方法如果做了实现,返回了定义的权限资源列表,
* Spring Security会在启动时校验每个ConfigAttribute是否配置正确,
* 如果不需要校验,这里实现方法,方法体直接返回null即可。
* @return
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 方法返回类对象是否支持校验,
* web项目一般使用FilterInvocation来判断,或者直接返回true
* @param clazz
* @return
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
- AccessDecisionManager
@Component
public class UserAccessDecisionManager implements AccessDecisionManager {
private static Logger log = LoggerFactory.getLogger(UserAccessDecisionManager.class);
@Autowired
private UserService userService;
/**
* 先查询此用户当前拥有的权限,然后与上面过滤器核查出来的权限列表作对比,
* 以此判断此用户是否具有这个访问权限,决定去留!所以顾名思义为权限决策器。
* @param authentication
* @param object
* @param configAttributes
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
if (authentication == null) {
throw new AccessDeniedException("当前访问没有权限");
}
ConfigAttribute ca = iterator.next();
//当前请求需要的权限
String needRole = ca.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
log.info("未登录");
throw new BadCredentialsException("未登录");
}
log.info("默认角色,没有权限");
throw new AccessDeniedException("权限不足!");
}
//当前用户所具有的权限
ManUser user = new ManUser();
String username = authentication.getPrincipal().toString();
user.setUsername(username);
ManUserVO manUserVO = userService.queryUserRelationAllRoleByUsername(user);
List<ManRole> roleList = manUserVO.getRoleList();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ManRole roleName : roleList) {
if (roleName.getRoleName().equals(needRole)) {
return;
}
}
}
log.info("权限不足!");
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
- AccessDeniedHandler
@Component
public class UnauthEntryPoint implements AccessDeniedHandler {
private static Logger log = LoggerFactory.getLogger(UnauthEntryPoint.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("自定义未授权统一返回类 ======》 没有权限访问");
ResponseUtil.error(response, Result.result("403","no login",null));
}
}
- SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static Logger log = LoggerFactory.getLogger(SecurityConfig.class);
private JwtUtil jwtUtil;
private RedisUtils redisUtils;
private DefaultPasswordEncoderUtil defaultPasswordEncoderUtil;
private UserDetailsService userDetailsService;
/*
权限决策器
*/
@Autowired
private UserAccessDecisionManager userAccessDecisionManager;
/*
权限资源器
*/
@Autowired
private UserFileterInvocationSecurityMetadataSource userFileterInvocationSecurityMetadataSource;
/*
获取登录表单额外参数
*/
@Autowired
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
/*
未授权统一返回类
*/
@Autowired
private UnauthEntryPoint unauthEntryPoint;
@Autowired
public SecurityConfig(UserDetailsService userDetailsService,
DefaultPasswordEncoderUtil defaultPasswordEncoderUtil,
JwtUtil jwtUtil,
RedisUtils redisUtils) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoderUtil = defaultPasswordEncoderUtil;
this.jwtUtil = jwtUtil;
this.redisUtils = redisUtils;
}
/**
* 配置设置,主要配置你的登录和权限控制、以及配置过滤器链。
*
* @param http
* @throws Exception
*/
//设置退出的地址和token,redis操作地址
@Override
protected void configure(HttpSecurity http) throws Exception {
log.info("进入了配置设置");
http.authorizeRequests().withObjectPostProcessor(
new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(userFileterInvocationSecurityMetadataSource);
o.setAccessDecisionManager(userAccessDecisionManager);
return o;
}
});
http.exceptionHandling().accessDeniedHandler(unauthEntryPoint)
//取消csrf防护
.and()
.csrf().disable()
//该配置是要求应用中所有url的访问都需要进行验证。我们也可以自定义哪些URL需要权限验证,哪些不需要
//所有请求都需要校验才能访问
.authorizeRequests().anyRequest().authenticated()
.and()
.cors()
//退出路径
.and()
.logout().logoutUrl("/admin/user/logout")
// 调用退出时的处理器
.addLogoutHandler(new TokenLogoutHandler( redisUtils,jwtUtil))
.and()
.formLogin()
.loginProcessingUrl("/admin/user/login").permitAll()
.successHandler(getLoginSuccessHandler())
.failureHandler(getLoginFailHandler())
.authenticationDetailsSource(authenticationDetailsSource)
.and()
// 认证过滤器
// .addFilter(new TokenLoginFilter(jwtUtil, redisUtils,authenticationManager()))
// 授权过滤器
.addFilter(new TokenAuthFilter(authenticationManager(),jwtUtil, redisUtils))
.httpBasic();
}
/**
* 调用userDetailsService和密码处理
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用SpringSecurity自己的校验规则,只能校验username和password
// auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoderUtil);
//使用自定义校验规则
auth.authenticationProvider(authenticationProvider());
}
/**
* 不进行认证的路径,可以直接访问,此时需要携带token,不然授权会抛出异常
* web.ignoring是直接绕开spring security的所有filter,直接跳过验证
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/user/getImage")
.antMatchers("/user/code");
}
/**
* 自定义校验
*/
@Bean
public AuthenticationProvider authenticationProvider() {
LocalAuthenticationProvider authenticationProvider = new LocalAuthenticationProvider();
return authenticationProvider;
}
// 登录成功处理器
@Bean
public LoginSuccessHandler getLoginSuccessHandler() {
return new LoginSuccessHandler();
}
// 登录失败处理器
@Bean
public LoginErrorHandler getLoginFailHandler() {
return new LoginErrorHandler();
}
}
五、注销登录
- 这里就比较简单了,一个简单的处理器就OK了,但是处理逻辑要根据实际业务进行实现哦
public class TokenLogoutHandler implements LogoutHandler {
private static Logger log = LoggerFactory.getLogger(TokenLogoutHandler.class);
private RedisUtils redisUtils;
private JwtUtil jwtUtil;
public TokenLogoutHandler(RedisUtils redisUtils, JwtUtil jwtUtil) {
this.redisUtils = redisUtils;
this.jwtUtil = jwtUtil;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
log.info("注销登录,拦截到了注销登录的请求");
//1、从header里面获取token
String token = request.getHeader("Authorization");
//2、token不为空,移除token,从Redis删除token
if(token != null){
try {
//解析token里的用户信息
PayloadDTO currentUser = jwtUtil.getJwtPayload();
log.info("解析后的用户数据为:"+currentUser.toString());
//将Redis里的用户信息及用户权限删除
redisUtils.remove(currentUser.getName());
} catch (ParseException e) {
e.printStackTrace();
} catch (JwtSignatureVerifyException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
}
}
}
六、演示
6.1、认证
6.2、授权
6.3、注销登录
七、流程图
直接复制搜索就OK啦:https://www.processon.com/view/link/61b187daf346fb6a6f182ed9