介绍:最近两天整理Spring Boot Security框架整合,里面也踩了一些坑记录下来提供参考
目录:1、spring security 基本配置 认证授权
2、spring security 短信验证 认证授权
3、spring security session管理机制
4、spring security cookie自动登录/持久化token权限验证
5、spring security thymeleaf-extras权限验证
6、spring security 特性方法说明
理论点:
spring security的核心是用户认证(Authentication)和用户授权(Authorization)。
用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。一般要求用户提供用户名和密码。
用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的
认证流程:这是一个过滤器链,所有的请求都会经过这些过滤器,响应也一样。其中绿色的是核心的过滤器,用来认证用户的身份
在系统启动后,springboot会自动加载这些过滤器,先进入FilterSecurityInterceptor拦截器,判断是否已认证,若认证通过可以访问资源,若没有认证,那么请求会进入这些绿色的过滤器先进行认证,如果认证失败,就会根据失败原因抛出相应的异常,进入到ExceptionTranslationFilter中,这个过滤器根据这些异常会做出相应的处理。FilterSecurityInterceptor是整个SpringSecurity过滤器的最后一环,是最终的守门人,它会去决定请求最终能否去访问到我们的Rest服务。在这一系列过滤器链中,绿色的过滤器可以通过配置去完成自定义配置,蓝色及黄色是不能控制的,它们一定都会生效且位置固定。
一、spring security 基本配置 认证授权
pom.xml加入坐标
1、security.Config文件
package com.lw.Config;
import com.lw.entity.Auth;
import com.lw.mapper.FunctiontMapper;
import com.lw.utils.CommunalUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationDetailsSource;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.List;
/**
* @Auther:LW
* @Date:2021年12月26日 9:13
*/
@Component
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 登录成功的处理
*/
@Autowired
private MyAuthenticationSuccessHandler successHandler;
/**
* 登录失败的处理
*/
@Autowired
private MyAuthenticationFailureHandler failureHandler;
/**
* 数据库验证用户权限信息
*/
@Autowired
private UserService userService;
/**
* 查询权限
*/
@Autowired
private FunctiontMapper functiontMapper;
/**
* cookie存储数据库token
*/
@Autowired
private DataSource dataSource;
/**
* 表单额外参数
*/
@Autowired
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
/**
* session处理
*/
@Autowired
private MySessionExpiredStrategy sessionExpiredStrategy;
//配置加密的方式
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//配置认证用户信息和授权
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
//解决静态资源被拦截的问题
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**","/css/**","/images/**","/js/**",
"/lib/**","/page/**","/bgimages/**");
}
//MD5加密
/*@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
//对输入的密码加密,这里暂时不用
@Override
public String encode(CharSequence charSequence) {
return null;
}
//加密密码与传入的密码对比
@Override
public boolean matches(CharSequence charSequence, String encodePassword) {
//encodePassword是数据库的密码,charSequence是输入的密码
return Md5Utils.md5((String)charSequence).equals(encodePassword);
}
});
}*/
/**
* 功能描述: 配置configure HttpSecurity
* @param: HttpSecurity
* @return: void
* @auther: LW
* @date: 2021年12月26日 10:52
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http
//开启HttpSecurity配置
.authorizeRequests();
//指定路径
//动态配置权限的授权
List<Auth> authList = functiontMapper.getfindAll();
for (Auth auth : authList) {
authorizeRequests.antMatchers(auth.getUrl()).hasAuthority(auth.getUrl());
}
authorizeRequests
.antMatchers("/platform/login").permitAll()//权限全部放开
.antMatchers("/platform/code").permitAll()
.antMatchers("/platform/loginIit").permitAll()
.antMatchers("/platform/check").permitAll()
.antMatchers("/platform/sessionInvalid").permitAll()
//登录认证之后方可使用 如果启用Cookie必须要使用用户授权认证
/*.antMatchers("/platform/meunInit").fullyAuthenticated()
.antMatchers("/platform/loginForm").fullyAuthenticated()
.antMatchers("/platform/welcome").fullyAuthenticated()*/
.antMatchers("/**").hasAuthority("ROLE_USER"); //用户自身授权可以使用
http.formLogin() //配置认证模式
.loginPage("/platform/login")//指定自定义的登录页地址
.loginProcessingUrl("/platform/loginForm")//登录成功跳转地址
//.usernameParameter("sysName")//匹配自定义登录页的name元素名称
//.passwordParameter("sysPassowrd")
.successHandler(successHandler)//登录成功的操作,如加入rememberMe/cookie自动登录指定重定向不能与loginProcessingUrl一样,否则会循环重定向
.failureHandler(failureHandler) //登录失败的操作
.authenticationDetailsSource(authenticationDetailsSource)//表单额外参数授权
.and().rememberMe()
.authenticationSuccessHandler(successHandler)//指定重定向不能与loginProcessingUrl一样,否则会循环重定向
//.userDetailsService(userService)//与authenticationSuccessHandler用其中一个
.tokenRepository(persistentTokenRepository())
.key(CommunalUtils.cookieKey)//密钥
.rememberMeCookieName(CommunalUtils.cookieName)//存在cookie的用户名[用于cookie名]
.tokenValiditySeconds(7 * 24 * 60 * 60)//生命周期,单位毫秒
.and().logout()
.logoutUrl("/platform/logOut")
.clearAuthentication(true) //清除身份认证信息 默认为true
.invalidateHttpSession(false) //设置session失效 默认为true
//.deleteCookies("platform-NameCookie")//删除Cookies
.logoutSuccessUrl("/page/")
/*.addLogoutHandler(new LogoutHandler() {//退出操作
@Override
public void logout(HttpServletRequest req, HttpServletResponse resp, Authentication auth) {}
})
.logoutSuccessHandler(new LogoutSuccessHandler() {//退出成功后跳转到登录
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException, ServletException {
resp.sendRedirect("/page/login.html");}
})*/
.and().csrf().disable()//禁用CSRF防护 防止跨站请求伪造访问
.sessionManagement() // 添加 Session管理器
.invalidSessionUrl("/platform/sessionInvalid")// Session失效后跳转到这个链接,与expiredSessionStrategy优先此配置
.maximumSessions(2)// 配置了最大Session并发数量
//.maxSessionsPreventsLogin(true)//只允许一个地点登录,如未退出二次登录不进,再次登陆会占用Session会抛异常,慎用
.expiredSessionStrategy(sessionExpiredStrategy);
//http.cors();//允许跨域发出XMLHttpRequest请求
//http.headers().frameOptions().disable(); //开启允许iframe 嵌套
}
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 如果token表不存在,使用下面语句可以初始化该表;若存在,请注释掉这条语句,否则会报错。
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
//设定cookie登录访问权限方法重新
/* @Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8082/platform"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}*/
}
2、权限配置
3、登录/退出配置
4、禁用CSRF,默认开启
5、配置用户登录授权
UserService.java
package com.lw.Config;
import com.lw.entity.Auth;
import com.lw.entity.SystemUser;
import com.lw.mapper.FunctiontMapper;
import com.lw.mapper.SystemUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
/**
* @Auther:LW
* @Date:2021年12月26日 9:13
*/
@Service
public class UserService implements UserDetailsService {
@Autowired
private SystemUserMapper systemUserMapper;
@Autowired
private FunctiontMapper functiontMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SystemUser systemUser = new SystemUser();
systemUser.setUsername(username);
SystemUser user = systemUserMapper.getSysUser(systemUser);
if(user==null){
throw new UsernameNotFoundException("用户不存在");
}
List<Auth> authList = functiontMapper.findAuthByUsername(username);
//赋予用户自有权限
if(authList!=null&&authList.size()>0){
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Auth auth : authList) {
authorities.add(new SimpleGrantedAuthority(auth.getUrl()));
}
//赋予用户共用权限
List<Auth> authList2 = functiontMapper.findAuthShare();
for (Auth auth : authList2) {
authorities.add(new SimpleGrantedAuthority(auth.getUrl()));
}
user.setAuthorities(authorities);
}
//spring security 会自动在数据库来查询用户信息,判断密码是否正确
return user;
}
}
6、用户密码加密处理
6.1、配置默认加密方式 passwordEncoder().encode()进行加密 加密结果每次变更 验证不变
6.2、进行MD5自定义 重写加密
7、自定义登录
package com.lw.Config;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @Auther:LW
* @Date:2021年12月29日 9:13
* 登录自定义Bean
*/
@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new MyAuthenticationDetails(context);
}
}
8、登录成功处理
package com.lw.Config;
import com.lw.entity.SystemUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Auther:LW
* @Date:2021年12月29日 9:13
*/
@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
SystemUser user = (SystemUser) authentication.getPrincipal();
log.info("============登录成功:"+user.toString());
resp.sendRedirect("/platform/loginIndex");
}
/*log.info("=========session获取======");
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if(auth!=null){
SystemUser user = (SystemUser) auth.getPrincipal();
log.info("============user成功:"+user.toString());
}*/
}
9、登录失败处理
package com.lw.Config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
/**
* @Auther:LW
* @Date:2021年12月26日 9:13
*/
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler{
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
log.info("============登录失败============");
String respCode = "400";
String respMsg;
if(e instanceof LockedException){
respMsg="账户被锁定,无法登录";
}else if(e instanceof BadCredentialsException){
e.printStackTrace();
respMsg="用户名或密码错误";
}else if(e instanceof DisabledException){
respMsg="账户被禁用,无法登录";
}else if(e instanceof AccountExpiredException){
respMsg="账户已过期,无法登录";
}else if(e instanceof CredentialsExpiredException){
respMsg="密码已过期,无法登录";
}else if(e instanceof SessionAuthenticationException){
respMsg="账号已登录,只允许登录一个终端,请联系管理员";
}else{
respMsg="登录异常,请联系管理员";
}
respMsg = URLEncoder.encode(respMsg,"UTF-8");
resp.sendRedirect("/platform/login?respCode="+respCode+"&&respMsg="+respMsg);
}
}
10、登录自定义表单其他参数获取
package com.lw.Config;
import com.lw.utils.CommunalUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @Auther:LW
* @Date:2021年12月29日 8:58
* 登录自定义表单其他参数
*/
@Slf4j
public class MyAuthenticationDetails extends WebAuthenticationDetails {
public MyAuthenticationDetails(HttpServletRequest request) {
super(request);
String rememberme = request.getParameter(CommunalUtils.cookieType);
Cookie[] cookies = request.getCookies();
String cookiesValue = getCookie(cookies,CommunalUtils.cookieName);
if(rememberme==null && !cookiesValue.equals("")){
Cookie cookie = new Cookie(CommunalUtils.cookieName, null);
HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
cookie.setMaxAge(0);
cookie.setPath(StringUtils.hasLength(request.getContextPath()) ? request.getContextPath() : "/");
resp.addCookie(cookie);
HttpSession session = request.getSession();
session.setAttribute("perToken", CommunalUtils.tonkenDel);
}
}
public static String getCookie(Cookie[] cookies, String cookiesName){
for (Cookie cookie : cookies) {
if(cookiesName.equals(cookie.getName())){ return cookie.getValue();}
}
return "";
}
}
11、错误异常处理
11.1、配置bean
package com.lw.Config;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
/**
* @Auther:LW
* @Date:2021年12月26日 9:13
*/
@Configuration
public class WebServerAutoConfiguration {
@Bean
public ConfigurableServletWebServerFactory webServerFactory(){
TomcatServletWebServerFactory factory=new TomcatServletWebServerFactory();
ErrorPage errorPage400=new ErrorPage(HttpStatus.BAD_REQUEST,"/error/400");
ErrorPage errorPage401=new ErrorPage(HttpStatus.UNAUTHORIZED,"/error/401");
ErrorPage errorPage403=new ErrorPage(HttpStatus.FORBIDDEN,"/error/403");
ErrorPage errorPage404=new ErrorPage(HttpStatus.NOT_FOUND,"/error/404");
ErrorPage errorPage415=new ErrorPage(HttpStatus.UNSUPPORTED_MEDIA_TYPE,"/error/415");
ErrorPage errorPage500=new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR,"/error/500");
factory.addErrorPages(errorPage400,errorPage401,errorPage403,errorPage404,errorPage415,errorPage500);
return factory;
}
}
11.2、配置异常Controller
package com.lw.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
/**
* @Auther:LW
* @Date:2021年12月29日 9:13
*/
@Controller
public class ErrorException implements ErrorController {
@Autowired
HttpServletRequest request;
@RequestMapping("/error")
public String getErrorPath() {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
switch (statusCode) {
case 404:
return "page/404";
case 403:
return "page/403";
default:
return "page/500";
}
}
@RequestMapping("/error/403")
public String error(){
return "page/403";
}
@RequestMapping("/error/404")
public String error404(){
return "page/404";
}
@RequestMapping("/error/500")
public String error500(){
return "page/500";
}
@RequestMapping("/error/400")
public String error400(){
return "page/400";
}
@RequestMapping("/error/401")
public String error401(){
return "page/401";
}
@RequestMapping("/error/415")
public String error415(){
return "page/415";
}
}
12、登录Controller
@GetMapping("/login")
public String initLogin(Model model,HttpServletRequest req,String respCode,String respMsg)throws IOException, ServletException {
model.addAttribute("platformLogin",env.getProperty("platform.login"));
if(respCode!=null && respCode.equals("400")){
model.addAttribute("respCode",respCode);
model.addAttribute("respMsg",respMsg);
}
if(!SecurityContextHolder.getContext().getAuthentication().getName().equals("anonymousUser")){
model.addAttribute("sysUser",SesionUtils.getSysUserSession());
return "index";
}
return "page/login";
}
@RequestMapping("/loginIndex")
public String loginIndex(Model model, SystemUser user,HttpServletRequest req) throws Exception{
model.addAttribute("platformLogin",env.getProperty("platform.login"));
model.addAttribute("sysUser",SesionUtils.getSysUserSession());
String perToken = (String) req.getSession().getAttribute("perToken");
if(perToken!=null && perToken.equals("del")){
persistentTokenRepository.removeUserTokens("admin");
}
return "index";
}
@RequestMapping("/logOut")
public String logOut(Model model, SystemUser user) throws Exception{
return "page/login";
}
13、登录页面login.html
<form th:action="@{/platform/loginForm}" class="layui-form login-bottom" id="loginFrom" method="post">
<div class="center">
<div class="item">
<span class="icon icon-2"></span>
<input type="text" name="username" lay-verify="required" placeholder="请输入登录账号" maxlength="24"/>
</div>
<div class="item">
<span class="icon icon-3"></span>
<input type="password" name="password" lay-verify="required" placeholder="请输入密码" maxlength="20">
<span class="bind-password icon icon-4"></span>
</div>
</div>
<div class="tip">
<input type="hidden" id="respCode" name="respCode" th:value="${respCode}">
<input type="hidden" id="respMsg" name="respMsg" th:value="${respMsg}">
<div class="layui-input-block">
<input id="remember-me" name="remember-me" title="保持登录" type="checkbox"/>
</div>
</div>
<div class="layui-form-item" style="text-align:center; width:100%;height:100%;margin:0px;">
<button class="login-btn" lay-submit="" lay-filter="login">立即登录</button>
</div>
</form>
下章讲解spring securityr短信验证码认证登录