Spring Security
Spring Security和Shiro
框架名称 | 特点 | 应用 | 常用组合 |
---|---|---|---|
Spring Security | 功能更丰富,社区资源丰富 | 中大型的项目 | Spring Boot/Spring Cloud + Spring Security |
Shiro | 上手更加的简单 | 小项目 | SSM + Shiro |
认证和授权
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
在Spring Security中就具体化为用户认证(Authentication)和用户授权(Authorization)两个部分。
用户认证
验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
通俗的说就是系统认为用户是否能登录。
用户授权
验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。
比如对某一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。
一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
通俗点讲就是系统判断用户是否有权限去做某些事情
SpringBoot整合SpringSecurity
启动项目,控制台中会打印出来密码:
我们在浏览器中输入http://localhost:8080/user/list
直接跳转到http://localhost:8080/login
输入完毕后点击Sign in 说明登录成功 跳转到http://localhost:8080/user/list
运行出结果
修改用户名和密码
spring.security.user.name=root
spring.security.user.password=123456
当前用户登录后,有没有权限去访问相应的请求
授权
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/*开启安全管理配置*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override//自定义认证
protected void configure(HttpSecurity http) throws Exception {
//authorizeRequests项目中的所有controller请求
//antMatchers不需要认证
//.permitAll();任何人都可以所以访问
http.authorizeRequests().antMatchers("/login").permitAll();
//验证角色商家可以发布商品
//http.authorizeRequests().antMatchers("/admin/pushGoods").hasRole("shangjia");
//验证有发布商品权限的所以用户
http.authorizeRequests().antMatchers("/admin/add","/delete","/pushGoods").hasAuthority("shangjiaquanxian");
//其他请求需要登录后访问
http.authorizeRequests().anyRequest().authenticated();
//目前使用表单登录
http.formLogin();
}
}
springsecurity的认证授权流程
Spring Security最核心的东西是一个过滤器链,这些过滤器在Spring boot启动的时候会帮我们配置上。
执行流程
文字描述话术:
具体的执行流程其实是一个过滤链:
username=root&&password=123456
1.UsernamePasswordAuthenticationFilter
拦截 /login post
从这个请求中 根据username 取出了用户名 root
根据password 取出了密码 123456
Authentication a1 = new Authentication(a1,setUsername("root"))
a1.setPassword("123456)
2.在第一步的方法中
我们继续调用ProviderManager.authenticate(a1 );
3.因为我们是表单提交 所以第三步
ProviderManager.authenticate(a1);
在authenticate内部继续调用DaoAuthenticationProvider.authencate(a1)
4.我们会继续调用InMemoryUserDetailsManager的loadUserByUsername(a1.username)
这个loadUserByUsername方法的返回值中包含 这个用户的用户名 密码 权限 角色信息
这个信息存放在一个叫UserDetails对象 这个对象中有当前登录这个用户的用户名root 密码123456对应的密码 权限角色信息
5.把a1中登录的密码 加密一下和 UserDetails中的密码进行比较 如果一样登录成功否则就是登录失败
6.如果登录成功 我们需要把UserDetails中的权限信息复制一份到a1对象中 返回a1对象
7.把a1对象存放在一个SecurityContext的上下文中?把a1对象存放到session中
1、用户向应用程序发起请求,请求需要经过Spring Security的过滤器链。
2、过滤器链首先会经过UsernamePasswordAuthenticationFilter过滤器,该过滤器判断请求是否是一个认证请求(如何知道是一个认证请求 拦截 对/login 的 POST 请求做拦截,校验表单中用户名,密码)。如果是认证请求,过滤器将获取请求中的用户名和密码,然后使用AuthenticationManager进行身份认证。
3、AuthenticationManager会根据用户名和密码创建一个Authentication对象,并将该对象传递给AuthenticationProvider进行认证。
4、AuthenticationProvider会根据传递过来的Authentication对象进行身份认证,并返回一个认证成功或失败的结果。
5、如果认证成功,UsernamePasswordAuthenticationFilter会将认证信息封装成一个Authentication对象,并将其放入SecurityContextHolder上下文中。
SecurityContextHolder
SecurityContextHolder上下文
session 一个用户在服务器上有一片空间 我们可以向这个空间中存放数据 只要是这个用户发送的请求 都可以共享这个空间的数据
SecurityContextHolder
获取securityContext,SecurityContextHolder的 getContext() 方法
SecurityContext securityContext = SecurityContextHolder.getContext();
1
从securityContext获取Authentication
Authentication authentication = securityContext.getAuthentication()
1
获取用户的信息,也就是UserDetails
UserDetails principal = (UserDetails)authentication.getPrincipal();
SecurityContextHolder 用来获取登录之后用户信息。Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这一策略非常方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据。
6、用户请求获取资源时,会经过FilterSecurityInterceptor过滤器,该过滤器会根据请求的URL和HTTP方法获取访问控制列表(Access Control List)。
7、Access Control List会包含访问资源所需要的权限信息,FilterSecurityInterceptor会将Authentication对象和Access Control List传递给AccessDecisionManager进行授权决策。
8、AccessDecisionManager会调用多个AccessDecisionVoter进行投票,并根据投票结果来决定当前用户是否有访问该资源的权限。如果用户被授权访问资源,应用程序将返回资源的响应结果。
总结就是首先经过认证过滤器实现认证,认证成功的话就会将用户信息存到authentication对象里面放到security上下文去(后续的权限校验需要获取到),这里面是包括权限的,之后再由AccessDecisionManager去根据相关策略进行权限鉴定
UsernamePasswordAuthenticationFilter 在org.springframework.security.web.authentication包下 可以查看源码
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
这也是第一个过滤器
过滤器来了先走这个方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);// 将登录成功信息存放在
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
ProviderManager
ProviderManager 在 org.springframework.security.authentication包下 可以查看源码 他实现了AuthenticationManager这个接口
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
}
因为是表单提交 所以调用的是DaoAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = this.determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw var6;
}
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
}
核心组件介绍:
Authentication
Authentication是一个接口,用来表示用户认证信息。
该对象主要包含了用户的详细信息(UserDetails)和用户鉴权时所需要的信息,如用户提交的用户名密码、Remember-me Token,或者digest hash值等。按不同鉴权方式使用不同的Authentication实现。
在用户登录认证之前相关信息会封装为一个Authentication具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的Authentication对象,然后把它保存在 SecurityContextHolder所持有的SecurityContext中,供后续的程序进行调用,如访问权限的鉴定等。
接口中的方法:
从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表,具体的详细解读如下:
getAuthorities(): 用户权限信息(权限列表),通常是代表权限的字符串列表;
getCredentials(): 用户认证信息(密码信息),由用户输入的密码凭证,认证之后会移出,来保证安全性;
getDetails(): 细节信息,Web应用中一般是访问者的ip地址和sessionId;
getPrincipal(): 用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等);
isAuthenticated: 获取当前 Authentication 是否已认证;
setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。
官方文档里说过,当用户提交登陆信息时,会将用户名和密码进行组合成一个实例UsernamePasswordAuthenticationToken,而这个类是Authentication的一个常用的实现类,用来进行用户名和密码的认证,类似的还有RememberMeAuthenticationToken,它用于记住我功能。
GrantedAuthority
Authentication的getAuthorities()方法返回一个 GrantedAuthority 对象数组。
GrantedAuthority该接口表示了当前用户所拥有的权限(或者角色)信息,用于配置 web授权、方法授权、域对象授权等。该属性通常由UserDetailsService 加载给 UserDetails。这些信息由授权负责对象AccessDecisionManager来使用,并决定最终用户是否可以访问某资源(URL或方法调用或域对象)。鉴权时并不会使用到该对象。
如果一个用户有几千个这种权限,内存的消耗将会是非常巨大的。
5.UserDetails
UserDetails存储的就是用户信息,它和Authentication接口类似,都包含了用户名,密码以及权限信息。
而区别就是Authentication中的getCredentials来源于用户提交的密码凭证,而UserDetails中的getPassword取到的则是用户正确的密码信息,认证的第一步就是比较两者是否相同,除此之外,Authentication#getAuthorities是认证用户名和密码成功之后,由UserDetails#getAuthorities传递而来。而Authentication中的getDetails信息是经过了AuthenticationProvider认证之后填充的。
其接口方法含义如下:
getAuthorites:获取用户权限,本质上是用户的角色信息。
getPassword: 获取密码。
getUserName: 获取用户名。
isAccountNonExpired: 账户是否过期。
isAccountNonLocked: 账户是否被锁定。
isCredentialsNonExpired: 密码是否过期。
isEnabled: 账户是否可用。
完成自定义的认证授权流程
1.UsernamePasswordAuthenticationFilter这个过滤器如何知道我们提交的是登录操作,
2.去数据库验证用户名和密码的操作应该写在什么地方?
我们需要进行自定义配置
package com.tyhxzy.springsecurity.config;
import org.springframework.context.annotation.Bean;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
// 开启安全框架
@EnableWebSecurity
// 针对方法开启方法前和方法后的权限验证还有 角色认证
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
/**
* 配置密码解析
* @return
*/
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置用户名和密码
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf防护 跨站请求防护
http.csrf().disable()
//表单登录
.formLogin()
//登录访问路径,与页面表单提交路径一致
.loginProcessingUrl("/login")
.and()
//认证配置
.authorizeRequests()
.antMatchers("/login").permitAll()
//任何请求
.anyRequest()
//都需要身份验证
.authenticated();
//配置退出
http.logout()
//退出路径
.logoutUrl("/logout")
;
}
}
导入以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
五张表
登录验证的是t_user表
权限和角色分布在t_role和t_permission表中
实体类的创建(略 )自行完成
对应的mapper文件和映射文件的创建(略)
表数据请参考资料中的offcnpe.sql文件
业务逻辑层的编写如下
我们自定义一个业务逻辑层实现类
package com.tyhxzy.springsecurity.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tyhxzy.springsecurity.entity.*;
import com.tyhxzy.springsecurity.mapper.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private TUserMapper userMapper;
@Autowired
private TUserRoleMapper tUserRoleMapper;
@Autowired
private TRoleMapper roleMapper;
@Autowired
private TRolePermissionMapper tRolePermissionMapper;
@Autowired
private TPermissionMapper tPermissionMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
LambdaQueryWrapper<TUser> userquery = new LambdaQueryWrapper<>();
userquery.eq(TUser::getUsername,s);
TUser user = userMapper.selectOne(userquery);
// 存放权限的集合
List<GrantedAuthority> ssp = null;
if(user==null){
throw new UsernameNotFoundException("用户名不存在");
}else{
ssp = new ArrayList<>();
LambdaQueryWrapper<TUserRole> q2 = new LambdaQueryWrapper<>();
q2.eq(TUserRole::getUserId,user.getId());
List<TUserRole> rids = tUserRoleMapper.selectList(q2);
// 遍历中间表 把所有的角色id查询回来
List<Integer> collect = rids.stream().map(sp -> sp.getRoleId()).collect(Collectors.toList());
List<TRole> tRoles = roleMapper.selectBatchIds(collect);
Stream<SimpleGrantedAuthority> simpleGrantedAuthorityStream = tRoles.stream().map(sp1 -> {
return new SimpleGrantedAuthority(sp1.getKeyword());
});
List<SimpleGrantedAuthority> collect1 = simpleGrantedAuthorityStream.collect(Collectors.toCollection(ArrayList::new));
ssp.addAll(collect1);
// 查询权限
// 也是先查询中间表
for(TRole item:tRoles) {
LambdaQueryWrapper<TRolePermission> q3 = new LambdaQueryWrapper<>();
q3.eq(TRolePermission::getRoleId,item.getId());
List<TRolePermission> tRolePermissions = tRolePermissionMapper.selectList(q3);
List<Integer> pids = tRolePermissions.stream().map(sp1 -> sp1.getPermissionId()).collect(Collectors.toList());
List<TPermission> tps = tPermissionMapper.selectBatchIds(pids);
Stream<SimpleGrantedAuthority> rty = tps.stream().map(sp1 -> {
return new SimpleGrantedAuthority(sp1.getKeyword());
});
List<SimpleGrantedAuthority> collect2 = rty.collect(Collectors.toCollection(ArrayList::new));
ssp.addAll(collect2);
}
}
return new User(s,user.getPassword(),ssp);
}
}
controller上编写
package com.tyhxzy.springsecurity.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/list")
@Secured(value = "ROLE_ADMIN")// 访问这个方法之前首先验证这个用户是否是ROLE_ADMIN这个角色
public String hh(){
return "哈哈";
}
@RequestMapping("/list1")
@PreAuthorize("hasAuthority('CHECKITEM_QUERY')") // 访问这个方法前首先验证这个用户是否有CHECKITEM_QUERY的权限
public String hh1(){
return "哈哈";
}
}
在浏览器中访问http://localhost:8080/user/list 因为未登录会直接被spring security打到登录页面,
在登录页面输入 admin 密码是123456 这个人的角色时ROLE_ADMIN可以直接在页面输出哈哈
如果在登录页面输入 xiaoming 密码是123456 这个人的角色不是ROLE_ADMIN 所以会跳转到403页面
自定义登录失败和权限认证失败的内容返回给客户端,不要直接打印出来403页面或者登录失败
AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
package com.tyhxzy.springsecurity.config;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 认证失败处理类 返回未授权
* 用来解决认证过的用户访问无权限资源时的异常
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
response.getWriter().print("没有访问权限!");
}
}
在WebSecurityConfig中添加如下代码:
@Autowired
private CustomAccessDeniedHandler deniedHandler;
// 设置已经登录过 但是没有权限访问要走的对象
http.exceptionHandling().accessDeniedHandler(deniedHandler);
AuthenticationFailureHandler 用来解决登录失败的异常
package com.tyhxzy.springsecurity.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException, IOException {
Map<String, Object> result = new HashMap<String, Object>();
result.put("msg", "登录失败: "+exception.getMessage());
result.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
String s = new ObjectMapper().writeValueAsString(result);
response.getWriter().println(s);
}
}
在WebSecurityConfig中添加如下代码:
@Autowired
@Autowired
private MyAuthenticationFailureHandler aa;
http.csrf().disable()
//表单登录
.formLogin()
//登录访问路径,与页面表单提交路径一致
.loginProcessingUrl("/login").failureHandler(aa)
总结
常见的过滤器
【1】WebAsyncManagerIntegrationFilter
将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
【2】SecurityContextPersistenceFilter
在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
【3】HeaderWriterFilter
用于将头信息加入响应中。
【4】CsrfFilter
用于处理跨站请求伪造。
【5】LogoutFilter
用于处理退出登录。
【6】UsernamePasswordAuthenticationFilter
用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改
【7】DefaultLoginPageGeneratingFilter
如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
【8】BasicAuthenticationFilter
检测和处理 http basic 认证。
【9】RequestCacheAwareFilter
用来处理请求的缓存。
【10】SecurityContextHolderAwareRequestFilter
主要是包装请求对象request。
【11】AnonymousAuthenticationFilter
检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
【12】SessionManagementFilter
管理 session 的过滤器。
【13】ExceptionTranslationFilter
处理 AccessDeniedException 和 AuthenticationException 异常。该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
【14】FilterSecurityInterceptor
可以看做过滤器链的出口。该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter过滤器进行捕获和处理。
【15】RememberMeAuthenticationFilter
当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
常见的验证授权流程
一、Session-Cookie 机制 (web应用中最常见的)
当服务端需要对访问的客户端进行身份认证时,常用的做法是通过session-cookie 机制流程
Session-Cookie 认证存在的问题:
当客户访问量增加,服务端需要存储大量的session会话,对服务端有很大考验
当服务端为集群时,用户登录其中一台服务器,会将session保存在该服务器的内存中,
但是当用户访问其他服务器时。会无法访问。(已经有了成熟的解决方案)可以采用使用缓存服务器来保证共享 第三方缓存来保存session由于依赖cookie,所以存在CSRF安全问题
前后端分离项目不共享session 演示问题
//关闭csrf防护 跨站请求防护
http.csrf().disable();
http
//认证配置
.authorizeRequests()
.antMatchers("/user/login","/user/add").permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.cors();// 开启跨域
// 设置登录失败的对象
//配置退出
http.logout()
//退出路径
.logoutUrl("/logout")
;
//http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
二、Token 认证机制:
//http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
使用Jwt
跨域概念
跨域是如何引起的?
浏览器的安全策略
http://localhost:8080/index.html
http://localhost:8082/demo.html
协议:http
localhost:主机名
ip:127.0.0.1
端口号:8080
协议 ip 端口号 三者都一致 叫同源
浏览器默认只允许该项目只能访问 和自己协议 ip 端口号一致的项目中的资源
http://localhost:8080/index.html 访问和自己 同源的demo.html是允许的
http://localhost:8080/index.html 访问 http://localhost:8082/demo.html 这就叫跨域
我们昨天是在 http://localhost:8080/index.html 去请求 http://localhost:8081/demo/a1 这默认浏览器是不允许的 这就叫跨域
Access to XMLHttpRequest at 'http://localhost:8081/demo/a1' from origin 'http://localhost:8080' has been blocked by CORS policy
origin 'http://localhost:8080' 从这个项目
请求 ttp://localhost:8081/demo/a1 另外一个项目
has been blocked by CORS policy 被跨域政策拦截住
No 'Access-Control-Allow-Origin' header is present on the requested resource.
在请求头上我们没找见 Access-Control-Allow-Origin
@CrossOrigin:告知该controller中的所有请求 允许任何项目访问(不限制必须是同源)
@CrossOrigin(origins = "http://localhost:8080") 告知该controller请求只能由 http://localhost:8080这个源访问
如果你在controller类上写了@CrossOrigin
那么服务器会向客户端的响应头中写入Access-Control-Allow-Origin=“*” 不限制源
如果你在controller类上写了@CrossOrigin(origins = "http://localhost:8080")
那么服务器会向客户端响应头中写入Access-Control-Allow-Origin=“http://localhost:8080”
浏览器拿到这个Access-Control-Allow-Origin=“http://localhost:8080” 之后 会进行比对 看自己在不在人家的范围内 如果不在 还是被跨域拦截,如果在 就说明人家允许你进行访问
浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求:若不同源,则发送跨域请求°。
。当我们发起跨域请求时,如果是非简单请求,浏览器会帮我们自动触发预检请求,也就是OPTIONS请求,用于确认目标资源是否支持跨域。如果是简单请求,则不会触发预检,直接发出正常请求。
·服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含Access-Control-All ow-origin字段,若配置过域名,则返回 Ac cess-Control-Allow-origin+对应配置规则里的域名的方式。
●浏览器根据接收到的响应头里的Access-Con trol-Allow-origin字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。
跨域问题
spring security 框架CsrfFilter权限高于@CrossOrigin
需要在配置类中关闭spring security的跨域保护
http.cors();
关闭表单登录
//http:formLogin.failureHandler();
配置放行界面
http.authorizeRequests().antMatchers("/demo/a1").permitAll();
http.csrf().disable();//关闭跨站请求伪造
告诉spring security不需要再把认证过的数据往session存放
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
自定义过滤器
表单登录失败
class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler
package com.mmkj.offcnpe.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
/**
* 适用于表单登录失败
*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json");
HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>();
stringObjectHashMap.put("message","登录失败");
stringObjectHashMap.put("code",2002);
//httpServletResponse.sendRedirect("/offcnpe/checkgroup/findAllCheckItems");
httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap));
}
}
已登陆权限不足跳转
package com.mmkj.offcnpe.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
//已登陆权限不足跳转
@Component
public class MyCustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json");
HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>();
stringObjectHashMap.put("message","权限不足");
stringObjectHashMap.put("code",2001);
httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap));
}
}
非表单登陆失败
package com.mmkj.offcnpe.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
//非表单登陆失败
@Component
public class MySecurity403 implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json");
HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>();
stringObjectHashMap.put("message","权限不足");
stringObjectHashMap.put("code",2003);
httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap));
}
}
表单登录成功处理器
class ForwardAuthenticationSuccessHandler implements AtuhenticationSuccessHandler
token验证登录状态
package com.mmkj.offcnpe.config;
import io.jsonwebtoken.Jwts;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
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.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;
//spring的过滤器,每次请求都进行拦截
@Component
public class OncePerOverRequestFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String, Object> template;
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String auth = httpServletRequest.getHeader("auth");//获取请求头中加密信息
if (auth != null && auth.equals("")) {
auth = auth.substring("Bearer".length());
Object nameObj = Jwts.parser().setSigningKey("yxh111").parseClaimsJws(auth).getBody().get("username");//解密获取用户名称
if (nameObj != null) {
LinkedHashMap<String, Object> redisDetail_Data = (LinkedHashMap<String, Object>) template.opsForValue().get(nameObj.toString());//authorties对象集合,格式不对
ArrayList<LinkedHashMap<String, String>> a2 = (ArrayList<LinkedHashMap<String, String>>) redisDetail_Data.get("authorities");
//List<GrantedAuthority> authority = AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", a2.stream().map(v -> v.get("authority")).collect(Collectors.toList())));
List<SimpleGrantedAuthority> collect = a2.stream().map(v -> v.get("authority")).map(v1 -> new SimpleGrantedAuthority(v1)).collect(Collectors.toList());
UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(redisDetail_Data.get("username").toString(), redisDetail_Data.get("password").toString(), collect);//创建UsernamePasswordAuthenticationToken对象
SecurityContextHolder.getContext().setAuthentication(upat);//放入上下文
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);//放行执行链
}
}
加载过滤器
package com.mmkj.offcnpe.config;
import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity//开启安全管理配置
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private MyCustomAccessDeniedHandler myCustomAccessDeniedHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private OncePerOverRequestFilter oncePerOverRequestFilter;
@Autowired
private MySecurity403 mySecurity403;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override//自定义认证
protected void configure(HttpSecurity http) throws Exception {
//http.formLogin().failureHandler(myAuthenticationFailureHandler);//表单登录失败跳转
http.authorizeRequests().antMatchers("/offcnpe/user/login").permitAll().anyRequest().authenticated();//放行请求
http.exceptionHandling().accessDeniedHandler(myCustomAccessDeniedHandler);//已登陆权限不足
http.exceptionHandling().authenticationEntryPoint(mySecurity403);//未登录权限不足
http.csrf().disable();//关闭跨站请求伪造
http.cors ();//关闭跨域保护
http.addFilterAt(oncePerOverRequestFilter, UsernamePasswordAuthenticationFilter.class);//登录成功请求头中状态验证
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//告诉spring security不需要再把认证过的数据往session存放
}
}
controller中登录判定
package com.mmkj.offcnpe.controller;
import com.mmkj.offcnpe.entity.TUser;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
/**
* <p>
* 前端控制器
* </p>
*
* @author yxh
* @since 2023-07-17
*/
@RestController
@RequestMapping("/offcnpe/user")
@CrossOrigin
public class UserController {
@Qualifier("userdetails")
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisTemplate<String, Object> template;
@RequestMapping("/login")
public String login(@RequestBody TUser user) {
if (!(SecurityContextHolder.getContext().getAuthentication() instanceof UsernamePasswordAuthenticationToken)){//判断当前上下文是否为空
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
if (bCryptPasswordEncoder.matches(user.getPassword(), userDetails.getPassword())) {
return "密码错误";
} else {
template.opsForValue().set(user.getUsername(), userDetails);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
JwtBuilder builder = Jwts.builder();
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR,1);
builder.setId("idxxx");
builder.setIssuer("张三");
builder.setExpiration(calendar.getTime());
builder.setIssuedAt(new Date());
builder.signWith(SignatureAlgorithm.HS256,"yxh111");
HashMap<String, Object> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put("username",userDetails.getUsername());
builder.addClaims(stringObjectHashMap);
return builder.compact();
}
}
return "登录成功";
}
@RequestMapping("/admin")
@Secured("ROLE_ADMIN")
public String admin() {
return "admin";
}
@RequestMapping("/publicList")
public String publicList() {
return "user";
}
@RequestMapping("/health")
@Secured("ROLE_HEALTH_MANAGER")
public String user() {
return "health-manager";
}
@RequestMapping("/delUser")
@PreAuthorize("hasAuthority('USER_DELETE')")
public String delUser() {
return "delUser";
}
}