目录
一、SpringSecurity简介
SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security登录认证主要涉及两个重要的接口 UserDetailService和UserDetails接口。
UserDetailService接口主要定义了一个方法 loadUserByUsername(String username)用于完成用户信息的查 询,其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接 口,完成数据库查询,该接口返回UserDetail。
UserDetail主要用于封装认证成功时的用户信息,即自己的User对象,但是最好是实现UserDetail接口,自定义用户对象。
二、集成SpringSecurity
环境版本:
- java version “1.8”
- SpringBoot 2.5.7.RELEASE
- SpringSecurity 5.5.3.RELEASE
1、引入依赖
新建一个springboot项目,然后pom文件夹引入springSecurity的依赖包(因为我项目里面用的thymeleaf引擎,所以多引入了一个依赖):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- thymeleaf和springSecurity的扩展依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
2、编写核心配置类
新建WebSecurityConfig配置类并继承WebSecurityConfigurerAdapter类。
代码如下:
/**
* <p>
* springSecurity安全访问配置。
* </p>
*
* @author 刘易彦
* @custom.date 2024/6/21 15:23
*/
@Configuration
@EnableWebSecurity
@EnableJdbcHttpSession
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private IUserService userDetailsService;
/**
* 在集群环境下控制会话并发的会话注册表
*/
@Resource
private FindByIndexNameSessionRepository<? extends Session> sessionRepository;
/**
* 自定义session失效策略
*/
@Resource
private SpringSecurityCustomInvalidSessionStrategy springSecurityCustomInvalidSessionStrategy;
/**
* 数据源
*/
@Resource
private DataSource dataSource;
@Resource
private CustomAuthenticationProvider customAuthenticationProvider;
/**
* 忽略URL
*/
protected static final String[] URLS = {
// API接口
"/web/login",
"/logout",
"/web/login-success",
"/web/logout",
"/doc.html",
"/favicon.ico",
"/v2/api-docs",
"/swagger-resources/**",
"/webjars/**"
};
/**
* 忽略静态资源
*/
protected static final String[] RESOURCES = {
"/images/**",
"/layui/**",
"/lib/**",
"/modules/**",
"/style/**",
"/views/**",
"/config.js",
"/css/**",
"/user/**",
"/js/**"
};
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(customAuthenticationProvider);
}
/**
* <p>
* 持久化token
* </p>
* spring Security中,默认是使用PersistentTokenRepository的子类InMemoryTokenRepositoryImpl,将token放在内存中,<br>
* 如果使用JdbcTokenRepositoryImpl,会创建表persistent_logins,将token持久化到数据库。
*
* @return {@link PersistentTokenRepository}
* @author lyy
* @custom.date 2024/6/21 15:23
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 启动创建表,创建成功后注释掉
//jdbcTokenRepository.setCreateTableOnStartup(true);
// 设置数据源
jdbcTokenRepository.setDataSource(this.dataSource);
return jdbcTokenRepository;
}
/**
* <p>
* 维护session注册信息
* </p>
*
* @return {@link SessionRegistry}
* @author lyy
* @custom.date 2024/6/21 15:23
*/
@Bean
public SessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(this.sessionRepository);
}
/**
* <p>
* HttpSecurity主要针对权限控制配置。
* </p>
*
* @param http {@link HttpSecurity}
* @author lyy
* @custom.date 2024/6/21 15:23
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允许匿名的url,填在下面
.antMatchers(URLS).permitAll()
.anyRequest().authenticated()
.and()
// 登录配置
.formLogin()
.usernameParameter("account")
.passwordParameter("password")
// 设置登陆页
.loginPage("/web/login")
// 设置登陆接口路径
.loginProcessingUrl("/web/doLogin")
// 设置登陆成功页
.defaultSuccessUrl("/web/login-success", true)
.permitAll()
// 认证失败处理器
//.failureHandler(this.authenticationFailureHandler)
.and()
.sessionManagement()
// 自定义session失效策略
.invalidSessionStrategy(this.springSecurityCustomInvalidSessionStrategy)
.invalidSessionUrl("/web/login?timeout=true")
.maximumSessions(1)
// 当达到最大值时,是否保留已经登录的用户
.maxSessionsPreventsLogin(false)
// 当达到最大值时,旧用户被踢出后的操作
.expiredUrl("/web/login?expire=true")
.sessionRegistry(this.sessionRegistry())
.and()
.and()
// 退出登录配置
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/web/logout-success")
.permitAll()
.invalidateHttpSession(true)
.and()
//.csrf()
//.disable()
// 允许嵌入iframe
.headers()
// 禁用缓存
.cacheControl()
.disable()
.frameOptions()
.disable();
// 关闭CSRF跨域
http.csrf().disable();
}
/**
* <p>
* WebSecurity主要针对全局请求忽略规则配置(比如说静态文件,比如说注册页面)、全局HttpFirewall配置、是否debug配置、全局SecurityFilterChain配置、privilegeEvaluator、expressionHandler、securityInterceptor等。
* </p>
*
* @param web {@link WebSecurity}
* @author lyy
* @custom.date 2024/6/21 15:23
*/
@Override
public void configure(WebSecurity web){
// web.ignoring直接绕开spring security的所有filter,直接跳过验证
// 设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers(RESOURCES);
}
}
其中configure(AuthenticationManagerBuilder auth)设定了自定义认证方式(具体实现后续文章中有详解)。
3、数据库建表
用户表:
建表后记得添加测试用户。
session策略表(用于自定义用户登录session失效策略):
spring_session:
spring_session_attributes:
4、自定义session失效策略
1.新建SpringSecurityCustomInvalidSessionStrategy类并实现InvalidSessionStrategy接口。
代码如下:
/**
* <p>
* SpringSecurity框架下自定义session失效策略
* </p >
*
* @author 刘易彦
* @custom.date 2024/1/4 15:02
*/
@Slf4j
@Component
public class SpringSecurityCustomInvalidSessionStrategy implements InvalidSessionStrategy {
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String requestUri = request.getRequestURI();
String sessionId = request.getSession().getId();
log.info("请求路径:{},session失效,获取一个新的session:{}", requestUri, sessionId);
// 如果是后台管理路径,则重定向到登录页面,并且提示登录已经超时
if (StringUtils.startsWith(requestUri, "/web")) {
// 执行请求重定向
response.sendRedirect("/web/login?timeout=true");
}
// 否则重定向到原页面,这样就能获取到一个新的session,继续保持正常访问
else {
// 执行请求重定向
response.sendRedirect(requestUri);
}
}
}
2.在核心配置类中的 configure(HttpSecurity http)添加
.invalidSessionStrategy(this.springSecurityCustomInvalidSessionStrategy)。
5、自定义认证
1.新建CustomAuthenticationProvider类并继承AbstractUserDetailsAuthenticationProvider类。
代码如下:
/**
* <p>
* 自定义springSecurity认证。
* </p>
*
* @author 刘易彦
* @custom.date 2024/6/21 17:23
*/
@Component
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private IUserService userService;
@SneakyThrows
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
String account = authentication.getPrincipal().toString(); // 获取账号
String password = authentication.getCredentials().toString(); // 获取密码
// 拿到后端查询到的账号密码信息进行比较
if (!StringUtils.equals(account, userDetails.getUsername())) {
throw new BadCredentialsException("账号或密码错误!");
}
if (!StringUtils.equals(password, userDetails.getPassword())) {
throw new BadCredentialsException("账号或密码错误!");
}
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//查询用户角色权限
UserDetails userDetails = userService.loadUserByUsername(username);
if (userDetails == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return userDetails;
}
}
我这里的项目因为是在内网中部署的,所以没有使用加密传输,也没有弄复杂的验证码功能,如有需要可以自行添加。
2.核心配置类添加认证方式
代码如下:
@Resource
private CustomAuthenticationProvider customAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(customAuthenticationProvider);
}
6、重写loadUserByUsername方法
因为SpringSecurity认证流程:loadUserByUsername()方法内部实现。并且只有这个方法能获取到前端登录的账号,因为我们需要通过用户账号去查询数据库存储的用户信息,所以我们需要重写此方法(我这里仅展示了用户信息的查询,如有角色、权限等的信息,都可以在这里进行查询并封装到SpringSecurityActiveUser类里面)。
代码如下:
/**
* @ClassName: CustomUserDetailsService
* @Description:
* @Params:
* @Author: lyy on 2022/3/21 20:37
*/
@Service("userDetailsService")
public class userDetailsServiceImpl extends ServiceImpl<IUserMapper, UserEntity> implements IUserService{
@Resource
private IUserMapper userMapper;
/**
* <p>
* 根据用户名获取用户
* </p >
*
* @param username 用户名
* @return {@link userDetailsServiceImpl}
* @author lyy
* @custom.date 2024/6/21 14:05
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 用户信息
LambdaQueryWrapper<UserEntity> userQw = new LambdaQueryWrapper<>();
userQw.eq(UserEntity::getUserAccount, username);
UserEntity userInfo = userMapper.selectOne(userQw);
return new SpringSecurityActiveUser(userInfo.getUserId(), userInfo.getUserAccount(), userInfo.getUserName(),
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}
/**
* <p>
* 在线用户
* </p>
*
* @author 刘易彦
* @since 2024-07-10
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class SpringSecurityActiveUser extends User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 用户账号
*/
private String userAccount;
/**
* 用户名
*/
private String userName;
public SpringSecurityActiveUser(Long userId, String userAccount, String userName,
Collection<? extends GrantedAuthority> authorities) {
super(userAccount, "", authorities);
this.userId = userId;
this.userAccount = userAccount;
this.userName = userName;
this.funAuthority = funAuthority;
}
}
7、登录页面和接口
css代码如下:
<style>
.layadmin-user-login-body {
position: relative;
background: #fff;
border-radius: 10px;
border: 1px solid rgba(34, 36, 38, .15) !important;
box-shadow: 8px 8px 8px #1a36279c;
}
.layadmin-user-login-header h2 {
margin-bottom: 10px;
font-weight: 600;
font-size: 40px;
color: #373737;
text-shadow: 8px 8px 8px #1a36273b;
}
h2 > img {
margin-right: 18px;
}
.layadmin-user-login-main {
width: 500px;
}
.layadmin-user-login-box {
padding: 50px 40px 30px;
}
.layadmin-user-login-icon {
line-height: 52px;
}
.layui-input, .layui-textarea, .layui-btn {
height: 52px;
line-height: 52px;
font-size: 16px;
}
</style>
html代码如下:
<img style="width: 100%;height: 100%;position: absolute;" class="disb imgset" alt="" th:src="@{/images/background.png}"/>
<div class="layadmin-user-login layadmin-user-display-show" id="LAY-user-login" style="display: none;">
<div class="layadmin-user-login-box layadmin-user-login-header">
<!-- <h2><img th:src="@{/images/logo2.png}"/>芯钛oa人事管理平台</h2>-->
</div>
<div class="layadmin-user-login-main">
<div class="layadmin-user-login-box layadmin-user-login-body layui-form">
<div class="layui-form-item">
<label class="layadmin-user-login-icon layui-icon layui-icon-username"
for="LAY-user-login-username"></label>
<input type="text" name="account" id="LAY-user-login-username"
placeholder="账号" class="layui-input" autocomplete="on">
</div>
<div class="layui-form-item">
<label class="layadmin-user-login-icon layui-icon layui-icon-password"
for="LAY-user-login-password"></label>
<input type="password" name="password" id="LAY-user-login-password"
placeholder="密码" class="layui-input">
</div>
<div class="layui-form-item login-check" th:if="${param.error}">
<label style="color: red;">
账号或密码错误,请重新输入!
</label>
</div>
<div class="layui-form-item login-check" th:if="${param.timeout}">
<label style="color: red;">
登录已超时,请重新登录!
</label>
</div>
<div id="accountCheck" class="layui-form-item login-check" style="display: none;">
<label style="color: red;">
账号不能为空!
</label>
</div>
<div id="passwordCheck" class="layui-form-item login-check" style="display: none;">
<label style="color: red;">
密码不能为空!
</label>
</div>
<!--<div class="layui-form-item" style="margin-bottom: 20px;">
<input type="checkbox" name="remember" lay-skin="primary" title="记住密码">
</div>-->
<div class="layui-form-item">
<button class="layui-btn layui-btn-fluid" lay-submit lay-filter="LAY-user-login-submit">登 入</button>
</div>
</div>
</div>
</div>
js代码如下:
let ctxPath = /*[[@{/}]]*/;
layui.config({
base: ctxPath // 静态资源所在路径
}).extend({
index: 'lib/index' //主入口模块
}).use(['index','common','form'], function () {
let $ = layui.$, form = layui.form, common = layui.common;
form.render();
//提交
form.on('submit(LAY-user-login-submit)', function () {
login();
});
$('.layui-form').keydown(function (event) {
if (event.keyCode === 13) {
// 回车键被按下
event.preventDefault(); // 阻止回车键默认行为(例如提交表单)
login();
}
});
/**
* 登录
*/
function login() {
// 先隐藏所有提示文字
$('.login-check').hide();
let $account = $('#LAY-user-login-username');
let account = $account.val();
if (typeof account === 'undefined' || account == null || account === '') {
$('#accountCheck').show();
$account.focus();
$account.css('border-color', 'red');
return false;
} else {
$('#accountCheck').hide();
$account.css('border-color', '');
}
let $password = $('#LAY-user-login-password');
let password = $password.val();
if (typeof password === 'undefined' || password == null || password === '') {
$('#passwordCheck').show();
$password.focus();
$password.css('border-color', 'red');
return false;
} else {
$('#passwordCheck').hide();
$password.css('border-color', '');
}
// 加密
/* account = common.rsaEncrypt(account);
password = common.rsaEncrypt(password);*/
// 提交表单
let tempForm = document.createElement('form');
tempForm.action = ctxPath + 'web/doLogin';
tempForm.method = 'post';
tempForm.style.display = 'none';
let accountInput = document.createElement('input');
accountInput.name = 'account';
accountInput.value = account;
tempForm.appendChild(accountInput);
let passwordInput = document.createElement('input');
passwordInput.name = 'password';
passwordInput.value = password;
tempForm.appendChild(passwordInput);
document.body.appendChild(tempForm);
tempForm.submit();
}
// 检查当前页面是否处于顶级窗口中
if (window.self !== window.top) {
// 重定向整个窗口到登录页面
window.top.location.href = ctxPath + 'web/login';
}
});
接口代码如下:
@ApiOperation(value = "访问首页")
@GetMapping("/index")
public ModelAndView index() {
SpringSecurityActiveUser activeUser = SpringSecurityUtils.getCurrentActiveUser();
ModelAndView mv = new ModelAndView("index");
return mv;
}
@ApiOperation(value = "登录页面")
@GetMapping("/login")
public ModelAndView login() {
ModelAndView mv = new ModelAndView("login");
return mv;
}
/**
* <p>
* 登录成功重定向到首页
* </p>
*
* @return 重定向首页URL
* @author lyy
* @custom.date 2024/6/18 21:09
*/
@GetMapping("/login-success")
public String loginSuccess() {
return "redirect:index";
}
/**
* <p>
* 退出登录成功重定向到首页
* </p>
*
* @return 重定向首页URL
* @author lyy
* @custom.date 2024/6/18 21:09
*/
@GetMapping("/logout-success")
public String logoutSuccess() {
return "redirect:index";
}
三、总结
通过本文的介绍,我们集成了SpringSecurity来实现了用户登录认证以及自定义认证方式。
标签:web,account,return,SpringBoot,自定义,SpringSecurity,login,password,public From: https://blog.csdn.net/milk_yan/article/details/143672569