SpringBoot整合Security入门教程
前言
严格来说,这并不算什么教程,只是记录我刚开始接触SpringSecurity所遇到的坑,最后成功实现的过程。网上的教程五花八门,尝试了好多都没运行成功,最后参考了这篇 springboot 集成 spring security 详细 附代码最终才成功,所以这里记录下自己的步骤,希望可以帮助和我一样的小白。
一、所需文件概述
下面来稍微介绍一下:
a、controller层和login.html页面不必多说
b、UserInfo实现了UserDetails
c、SecurityConfig配置了登录授权和身份认证等信息
d、UserDetailsServiceImpl实现了UserDetailsService类
e、MyAuthenticationProvider实现了AuthenticationProvider类
二、详细介绍内容
1.UserInfo.class
package com.braisedpanda.web.security;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
/**
* @program: admin-dashboard
* @description:
* @create: 2019-11-21 09:08
**/
@Data
public class UserInfo implements UserDetails,Serializable {
/*这些属性是自己可以定义的,可以不用定义太多,仅仅用来临时作为验证用的,一般不会存入数据库*/
private static final long serialVersionUID = 1L;
private String username; //用户名
private String password; //密码
private String role; //角色
private Integer status; //用户状态 禁用|启动
private boolean accountNonExpired; //账户是否未过期,过期无法验证
private boolean accountNonLocked; //指定用户是否解锁,锁定的用户无法进行身份验证
private boolean credentialsNonExpired; //指示是否已过期的用户的凭据(密码),过期的凭据防止认证
private boolean enabled; //是否可用 ,禁用的用户不能身份验证
public UserInfo(String username, String password, String role, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) {
this.username = username;
this.password = password;
this.role = role;
this.accountNonExpired = accountNonExpired;
this.accountNonLocked = accountNonLocked;
this.credentialsNonExpired = credentialsNonExpired;
this.enabled = enabled;
}
/*下面的override都是必须实现的方法*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
UserInfo: 上面的注释也说了,这个类是用作security验证用的,虽然和我们常用自己定义的User类很相似,但是UserInfo一般不存入数据库,我们仍然需要自己定义一个User类作为数据库存储,之后验证用户登录的时候,会从这个user表中查用户
2.SecurityConfig.class
package com.braisedpanda.web.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.filter.CharacterEncodingFilter;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @program: admin-dashboard
* @description: 配置拦截和验证
* @create: 2019-11-21 08:46
**/
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Resource
private DataSource dataSource;
@Autowired
private PersistentTokenRepository tokenRepository;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired //必须
private MyAuthenticationProvider provider; //注入我们自己的AuthenticationProvider
/**
* @Description: 授权配置(必须)
* @Date: 2019/11/21 0021
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/**登录授权*/
http
.authorizeRequests().antMatchers("/css/**","/js/**","/fonts/**","/login/**","/sass/**","/fonts/**").permitAll() //静态资源不被拦截
.and()
.authorizeRequests().antMatchers("/regist/**","/toregist/**").permitAll() //用户注册页面不被拦截
.and()
.authorizeRequests().anyRequest().authenticated() //其余页面都需要认证(只有登录后才可以正常访问)
.and()
.formLogin().loginPage("/tologin") //表单登录页面
.loginProcessingUrl("/login") //表单登录方法
.permitAll()
.and()
.logout()
.logoutUrl("/logout") //退出登录,退出登录时,会清楚remember-me中响应的登录token
.and()
.rememberMe()
.tokenRepository(tokenRepository) // 设置数据访问层,勾选remember-me时,会存入token在数据库
.rememberMeServices(rememberMeServices()) // 读取数据库中remember-me的相关token
.key("INTERNAL_SECRET_KEY")
.tokenValiditySeconds(60*30) // 记住我的时间(秒)
.and()
.csrf().disable();
/**session失效管理*/
http.sessionManagement().invalidSessionUrl("/tologin");
/**退出删除cookie*/
http.logout().deleteCookies("JESSIONID");
/**解决中文乱码问题*/
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceEncoding(true);
http.addFilterBefore(filter,CsrfFilter.class);
}
/**
* @Description: 验证身份类(必须)
* @Date: 2019/11/21 0021
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.authenticationProvider(provider);
}
/**生成remember-me token存入数据库**/
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
/** remember-me 在指定的时间内可以免登陆**/
@Bean
public RememberMeServices rememberMeServices() {
JdbcTokenRepositoryImpl rememberMeTokenRepository = new JdbcTokenRepositoryImpl();
// 此处需要设置数据源,否则无法从数据库查询验证信息
rememberMeTokenRepository.setDataSource(dataSource);
// 此处的 key 可以为任意非空值(null 或 ""),但必须和前面保持一致
PersistentTokenBasedRememberMeServices rememberMeServices =
new PersistentTokenBasedRememberMeServices("INTERNAL_SECRET_KEY", userDetailsService, rememberMeTokenRepository);
return rememberMeServices;
}
}
SecurityConfig.class: 注释说的挺清楚了,刚入门可以不用配置这么复杂,保留login的相关配置就可以了,remember-me的配置都可以删掉
3.UserDetailsServiceImpl.class
package com.braisedpanda.web.security;
import com.braisedpanda.commons.model.User;
import com.braisedpanda.web.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
/**
* @program: admin-dashboard
* @description: 通过用户名在数据库中查找角色信息
* @create: 2019-11-21 09:12
**/
@Component
public class UserDetailsServiceImpl implements UserDetailsService{
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUsername(username);
if(user != null){
String name = user.getUsername();
String password = user.getPassword();
UserInfo userInfo=new UserInfo(name, password, "ROLE_ADMIN", true,true,true, true);
userInfo.setStatus(user.getStatus());
return userInfo;
}else
return null;
}
}
UserDetailsServiceImpl.class: 这个功能一目了然,就是根据表单传入的username来查找数据库中响应的user,值得注意的是,重写的这个方法返回类型是UserDetails,上面我们定义的UserInfo就是实现了UserDetails,所以返回UserInfo, userInfo中存入了通过数据库表中根据username所查到的用户的用户名和密码,当然还可以查权限相关的信息,这里没有涉及到权限,就回传了ROLE_ADMIN,没啥作用,不要被这个忽悠了。
4.MyAuthenticationProvider.class
package com.braisedpanda.web.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* @program: admin-dashboard
* @description: 验证用户名和密码是否正确,如果错误,抛出错误信息
* @create: 2019-11-21 09:16
**/
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
/**
* 注入我们自己定义的用户信息获取对象
*/
@Autowired
private UserDetailsServiceImpl userDetailService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();// 这个获取表单输入中返回的用户名;
String password = (String) authentication.getCredentials();// 这个是表单中输入的密码;
// 这里构建来判断用户是否存在和密码是否正确
UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 这里调用我们的自己写的获取用户的方法;
if (userInfo == null) {
throw new BadCredentialsException("用户名不存在");
}
if (!userInfo.getPassword().equals(password)) {
throw new BadCredentialsException("密码不正确");
}
//这个判断状态可加可不加的,也可以在userinfo定义其他的条件,在这里判断,比如定义过期时间什么的,随便定义
if(userInfo.getStatus() == 0 ){
throw new BadCredentialsException("用户状态被禁用");
}
Collection<? extends GrantedAuthority> authorities = userInfo.getAuthorities();
// 构建返回的用户登录成功的token
return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
MyAuthenticationProvider.class:
这个类使用了UserDetailsServiceImpl 传过来的userinfo,根据userinfo中的信息来比对表单传过来的username和password,当然也可以判断其他条件,比如:用户状态,用户是否过期… 这些条件只需在userinfo中自己灵活定义即可。抛出的异常信息 throw new BadCredentialsException(“xxxxx”) 可以在前端页面进行显示
5.controller(不必多说,就是最简单的跳转)
package com.braisedpanda.web.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* @program: admin-dashboard
* @description:
* @create: 2019-11-20 10:31
**/
@RestController
public class LoginController {
/**
* @Description: 跳转到登录界面
* @Date: 2019/11/20 0020
*/
@RequestMapping("/tologin")
public ModelAndView tologin(){
return new ModelAndView("login");
}
/**
* @Description: 用户登录
* @Date: 2019/11/20 0020
*/
@RequestMapping("/login")
public ModelAndView login(){
return new ModelAndView("index");
}
/**
* @Description: 退出登录
* @Date: 2019/11/20 0020
*/
@RequestMapping("/logout")
public ModelAndView logout(){
return new ModelAndView("login");
}
@RequestMapping("/regist")
public ModelAndView regist(){
return new ModelAndView("login");
}
@RequestMapping("/toregist")
public ModelAndView toregist(){
return new ModelAndView("regist");
}
}
6.login.html(最简单的登录表格就可以了,这里贴下我的提供参考)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--===============================================================================================-->
<link rel="icon" type="image/png" href="/login/images/icons/favicon.ico"/>
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/vendor/bootstrap/css/bootstrap.min.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/fonts/font-awesome-4.7.0/css/font-awesome.min.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/fonts/Linearicons-Free-v1.0.0/icon-font.min.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/vendor/animate/animate.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/vendor/css-hamburgers/hamburgers.min.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/vendor/animsition/css/animsition.min.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/vendor/select2/select2.min.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/vendor/daterangepicker/daterangepicker.css">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="/login/css/util.css">
<link rel="stylesheet" type="text/css" href="/login/css/main.css">
<!--===============================================================================================-->
</head>
<body style="background-color: #666666;">
<div class="limiter">
<div class="container-login100">
<div class="wrap-login100">
<form class="login100-form validate-form" th:action="@{/login}" method="post">
<span class="login100-form-title p-b-43">
登录
</span>
<div th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}"></div>
<div class="wrap-input100 validate-input" data-validate = "用户名不能为空">
<input class="input100" name="username">
<span class="focus-input100"></span>
<span class="label-input100">用户名</span>
</div>
<div class="wrap-input100 validate-input" data-validate="密码不能为空">
<input class="input100" type="password" name="password">
<span class="focus-input100"></span>
<span class="label-input100">密码</span>
</div>
<div class="flex-sb-m w-full p-t-3 p-b-32">
<div class="contact100-form-checkbox">
<input class="input-checkbox100" id="ckb1" type="checkbox" name="remember-me">
<label class="label-checkbox100" for="ckb1">
记住我
</label>
</div>
<div>
<a href="#" class="txt1">
忘记密码?
</a>
</div>
</div>
<div class="container-login100-form-btn">
<input class="login100-form-btn" type="submit" value="登录">
</input>
</div>
</form>
<div class="login100-more" style="background-image: url('/login/images/bg-011.jpg');">
</div>
</div>
</div>
</div>
<!--===============================================================================================-->
<script src="/login/vendor/jquery/jquery-3.2.1.min.js"></script>
<!--===============================================================================================-->
<script src="/login/vendor/animsition/js/animsition.min.js"></script>
<!--===============================================================================================-->
<script src="/login/vendor/bootstrap/js/popper.js"></script>
<script src="/login/vendor/bootstrap/js/bootstrap.min.js"></script>
<!--===============================================================================================-->
<script src="/login/vendor/select2/select2.min.js"></script>
<!--===============================================================================================-->
<script src="/login/vendor/daterangepicker/moment.min.js"></script>
<script src="/login/vendor/daterangepicker/daterangepicker.js"></script>
<!--===============================================================================================-->
<script src="/login/vendor/countdowntime/countdowntime.js"></script>
<!--===============================================================================================-->
<script src="/login/js/main.js"></script>
</body>
</html>
三、效果演示
登录界面
登录失败
好了,基本的入门核心内容就是这么多了,最后附上我的代码,仅供参考代码地址
希望能帮助到和我一样第一次第一次接触security的小白们