<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
<version>2.6.3version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
导入了启动器spring security就生效了,但是显然默认的认证授权不适用生产环境,所以我们需要自己配置认证授权的过滤器
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器
图中只展示了核心过滤器;
UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后得登录请求;
ExceptionTranslationFilter:处理过滤器链中抛出得AccessDeniedException;
FilterSecurityInterceptor:负责权限校验得过滤器。
Authentication:他的实现类,表示当前访问系统的用户,封装了相关的用户信息。
AuthenticationManager:定义了认证Authentication的方法,认证相关的核心接口,也是发起认证的出发点
UserDetailService:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。
UserDetail:提供核心用户信息,通过UserDetailService根据用户名获取u哦那个胡信息要封装成UserDeail对象返回,然后将这些信息封装到Authentication对象中,然后通过SecurityContentHolder.setAuthentication()方法,将Authentication对象封装到SecurityContentHolder对象中(上图第十步)。
ProviderManager:AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。
只保留了关键认证部分的ProviderManager源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 维护一个AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 依次认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息,则直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//移除密码
((CredentialsContainer) result).eraseCredentials();
}
//发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
//执行到此,说明没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
登录
1.自定义登录接口
调用ProviderManager的方法进行认证,认证通过生成jwt
把用户信息存入redis中
2.自定义UserDetailsService
查询数据库获取用户信息
校验
1.定义jwt过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder中
实现UserDetailsService接口,重写loadUserByUsername方法,将用户信息封装搭配UserDetails对象中。
package com.zhijin.springcloud.security.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.entity.LoginUser;
import com.zhijin.springcloud.security.entity.RoleBO;
import com.zhijin.springcloud.security.entity.SecurityUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
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.List;
import java.util.stream.Collectors;
@Service
public class SecurityUserService implements UserDetailsService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenManager tokenManager;
@Autowired
private RedisTemplate redisTemplate;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//获取登录用户信息
Object obj = redisTemplate.opsForValue().get("user:ZJGG_userInfo_" + userName);
LoginUser loginUser = new LoginUser();
//如果redis中没有该数据,则从数据库中获取(如果不是单点过来的,则直接使用fegin调用用户微服务,查询数据库
if (BeanUtil.isEmpty(obj)){
//从数据库中获取
loginUser = gainUser();
}else {
loginUser = Convert.convert(LoginUser.class, obj);
}
Long userId = loginUser.getId();
String token = tokenManager.createToken(userId.toString());
//把完整用户信息存入redis作为key
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(loginUser);
//获取权限列表
List<String> permissionValueList = gainRoleList().stream().map(RoleBO::getRoleCode).collect(Collectors.toList());
securityUser.setPermissionValueList(permissionValueList);
redisTemplate.opsForValue().set("token:" + userName + "_" + token,securityUser);
return securityUser;
}
/**
* 模拟获取用户信息
* @return
*/
private LoginUser gainUser(){
LoginUser loginUser = new LoginUser();
loginUser.setId(1L);
loginUser.setNickName("纸巾哥哥");
loginUser.setPassword("password");
loginUser.setSalt("");
loginUser.setUsername("zhijingege");
return loginUser;
}
/**
* 模拟获取权限列表
* @return
*/
private List<RoleBO> gainRoleList(){
List<RoleBO> roleList = new ArrayList<>();
RoleBO role = new RoleBO();
role.setId(1L);
role.setRoleCode("R0001");
role.setRoleName("角色0001");
roleList.add(role);
return roleList;
}
}
在接口中我们通过AuthenticationManager的authenticate方法进行用户认证,所以需要在SecurityConfig中把AuthenticationManager注入容器中,同时需要放行该接口;
package com.zhijin.springcloud.security.service;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.entity.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenManager tokenManager;
public Object login(LoginUser user) {
//AuthenticationManager 认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//判断认证是否通过
if (Objects.isNull(authenticate)){
throw new RuntimeException("认证失败");
}
//认证通过,把完整的用户信息存入redis(略)
//使用userName(或者userId,只要是唯一的就可以)生成token并返回给前端
String token = tokenManager.createToken(user.getUsername());
return token;
}
}
除了自定义登录接口,还可以通过配置认证过滤器实现登录
package com.zhijin.springcloud.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.entity.LoginUser;
import com.zhijin.springcloud.security.entity.SecurityUser;
import com.zhijin.springcloud.security.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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;
import java.util.ArrayList;
/**
* 认证过滤器
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
try {
//获取用户从页面输入的登录名和密码。如果是单点过来的登录信息,需要通过单点的token获取用户的登录信息处理过后再封装到user中
LoginUser user = new ObjectMapper().readValue(req.getInputStream(), LoginUser.class);
// 这里的第一个参数是后面UserDetailService接口方法loadUserByUsername中的参数,
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 登录成功
* @param req
* @param res
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.fail(200,"登录成功");
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseUtil.fail(500,"登录失败");
}
}
package com.zhijin.springcloud.security.filter;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.utils.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
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 org.springframework.util.StringUtils;
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.Collection;
import java.util.List;
/**
* token认证
*/
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
logger.info("================="+req.getRequestURI());
//获取token
String token = req.getHeader("token");
if(org.apache.commons.lang3.StringUtils.isEmpty(token)) {
chain.doFilter(req, res);
return;
}
if(req.getRequestURI().indexOf("admin") == -1) {
chain.doFilter(req, res);
return;
}
//解析token
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
ResponseUtil.fail(500,e.getMessage());
}
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.fail(500,"登录失败");
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token置于header里
String token = request.getHeader("token");
if (token != null && !"".equals(token.trim())) {
String userName = tokenManager.getUserFromToken(token);
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
if (!StringUtils.isEmpty(userName)) {
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
return null;
}
}
package com.zhijin.springcloud.security.config;
import com.zhijin.springcloud.security.DefaultPasswordEncoder;
import com.zhijin.springcloud.security.TokenManager;
import com.zhijin.springcloud.security.UnauthorizedEntryPoint;
import com.zhijin.springcloud.security.filter.TokenAuthenticationFilter;
import com.zhijin.springcloud.security.filter.TokenLoginFilter;
import com.zhijin.springcloud.security.handler.CustomizeAuthenticationFailureHandler;
import com.zhijin.springcloud.security.handler.CustomizeAuthenticationSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
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;
/**
* 核心配置类
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenManager tokenManager;
@Autowired
private DefaultPasswordEncoder defaultPasswordEncoder;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomizeAuthenticationFailureHandler authenticationFailureHandler;
// @Autowired
// private CustomizeLogoutSuccessHandler logoutSuccessHandler;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置设置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEntryPoint())//异常处理
.and().formLogin().successHandler(authenticationSuccessHandler)//登录成功逻辑处理
.failureHandler(authenticationFailureHandler)//登录失败逻辑处理
.and().logout().permitAll()
// .logoutSuccessHandler(logoutSuccessHandler)//登出成功逻辑处理
.and().csrf().disable()//防止csrf攻击
.authorizeRequests()
.antMatchers("admin/**").hasAnyAuthority("admin")//只有指定角色才能访问admin路径下的
// .sessionManagement().maximumSessions(1)//同一个账号只能登录一次
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//不通过session获取SecurityContext
// .authorizeRequests()//授权请求
// .anyRequest().authenticated()//需要登录
// .anyRequest().permitAll()//所有请求通过
// .and().logout().logoutUrl("/admin/acl/index/logout")//登出页面
// .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()//登出处理
.and().addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))//认证过滤
.addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic()//授权过滤
;
}
/**
* 密码处理
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
/**
* 配置哪些请求不拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**",
"/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
);
}
}
anyRequest | 匹配所有请求路径
access | SpringEl表达式结果为true时可以访问
anonymous | 匿名可以访问
denyAll | 用户不能访问
fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
hasRole | 如果有参数,参数表示角色,则其角色可以访问
permitAll | 用户可以任意访问
rememberMe | 允许通过remember-me登录的用户访问
authenticated | 用户登录后可访问