工程Demo结构
工程demo地址:https://gitee.com/decent-cat/security.git
一. 使用
1.1 添加依赖包
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring-security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--social-web-->
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
</dependencies>
1.1 编写自己的登录页面
该登录页面包包含了验证码, 记住我功能
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/vue.js" type="text/javascript"></script>
<script src="js/axios.min.js" type="text/javascript"></script>
<script src="js/elementIndex.js" type="text/javascript"></script>
<link href="css/elementIndex.css" rel="stylesheet" type="text/css" />
<style>
* {
padding: 0;
margin: 0;
}
.el-header {
background: #d3dce6;
}
</style>
<script>
function refreshImageCode(imageTag) {
imageTag.src = '/validate/code?date=' + new Date();
}
</script>
</head>
<body>
<div id="app">
<el-container>
<el-header>XXXX</el-header>
<el-main>
<el-row>
<el-col :span="8" :offset="8">
<el-card class="box-card">
<el-tabs v-model="activeName">
<el-tab-pane label="用户名登录" name="usernameLogin">
<el-form :model="countLoginForm" ref="countLoginForm" label-width="70px">
<el-form-item label="用户名">
<el-input v-model="countLoginForm.username"></el-input>
</el-form-item>
<el-form-item label="密 码">
<el-input v-model="countLoginForm.password"></el-input>
</el-form-item>
<el-form-item label="验证码">
<el-input v-model="countLoginForm.validateCode" style="width: 75%;"></el-input>
<img style="width:23%; position: relative; top: 12px;" onclick="refreshImageCode(this)" src="/validate/code">
<!-- <img style="width:23%; position: relative; top: 12px;" @click="getValidateCodeImage" :src="validateCodeImage">-->
</el-form-item>
<el-form-item>
<el-checkbox v-model="countLoginForm['remember-me']" label="记住我"></el-checkbox>
</el-form-item>
<el-form-item>
<el-button @click.prevent="accounLogin" type="primary" style="width: 100%;">登录</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="短信码登录" name="smsLogin">
<el-form :model="smsLoginForm" :rules="rules" ref="smsLoginForm" label-width="70px">
<el-form-item label="手机号" prop="mobile">
<el-input v-model="smsLoginForm.mobile"></el-input>
</el-form-item>
<el-form-item label="验证码">
<el-input v-model="smsLoginForm.smsCode" style="width: 68%;"></el-input>
<el-button style="width: 30%;" :disabled="sendSmsBtnEnable" @click.prevent="sendSmsCode">{{sendSmsBtnText}}</el-button>
</el-form-item>
<el-form-item>
<el-button @click.prevent="smsLogin" type="primary" style="width: 100%;">登录</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</div>
</body>
<script>
new Vue({
el: '#app',
data() {
return {
rules: {
mobile: [
{required: true, message: '手机号不能为空', trigger: 'blur'},
{pattern: /^1([3578]\d|9[19])\d{8}$/, message: '目前只支持中国大陆手机号', trigger: 'blur'}
]
},
countLoginForm: {
username: '',
password: '',
'remember-me': false,
validateCode: '',
},
smsLoginForm: {
mobile: '',
smsCode: '',
},
sendSmsBtnEnable: false, //发送短信按钮的可用状态
sendSmsBtnText: '发送短信', //发送短信按钮上显示文字
activeName: 'usernameLogin', //默认账户登录表单激活
showErrorMsg: false, //错误信息是否展示
errorMsg: '', //错误信息内容
validateCodeImage: '',
}
},
mounted(){
this.getValidateCodeImage(); //获取图片验证码
},
methods: {
//账号登录
accounLogin() {
axios({
url: '/authentication/form',
method: 'post',
params: this.countLoginForm
}).then(resp => {
let datas = resp.data
if(datas.code > 0) {
window.location.href = "/user";
}else {
/**
this.showErrorMsg = true;
this.errorMsg = resp.data.msg;
*/
this.$notify({
title: '警告',
message: resp.data.msg,
type: 'warning'
});
}
});
},
sendSmsCode() {
this.$refs['smsLoginForm'].validate(valid => {
if(valid) {
this.secondDown(60);
// 判断手机号不能为空
axios.get('/sms/code?mobile=' + this.smsLoginForm.mobile);
}else {
return;
}
})
},
// 获取图片验证码,方便在纯粹的前后端分离的时候使用
getValidateCodeImage(){
axios({
url: '/validate/code',
method: 'get',
}).then(resp => {
this.validateCodeImage = "data:image/jpeg;base64," + resp.data;
})
},
//短信登录
smsLogin(loginType) {
axios({
url: '/authentication/sms',
method: 'post',
params: this.smsLoginForm
}).then(resp => {
let datas = resp.data
if(datas.code > 0) {
window.location.href = "/user";
}else {
/**
this.showErrorMsg = true;
this.errorMsg = resp.data.msg;
*/
this.$notify({
title: '警告',
message: resp.data.msg,
type: 'warning'
});
}
})
},
//发送短信倒计时
secondDown(seconds) {
if(seconds <= 0) {
this.sendSmsBtnEnable = false; //60秒倒计时后按钮可用
this.sendSmsBtnText = '发送短信';
}else {
this.sendSmsBtnEnable = true; // 按钮不可用
this.sendSmsBtnText = '已发送(' + seconds + ")";
seconds--;
setTimeout(() => this.secondDown(seconds), 1000);
}
}
}
})
</script>
</html>
1.2 自定义用户认证
// 该类的作用是处理用户登录名和密码
@Component
public class UserSecurityService implements UserDetailsService {
//日志打印
private static Logger logger = LoggerFactory.getLogger(UserSecurityService.class);
//数据库用户数据获取类 @Service
@Resource
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("用户名:" + username); //打印用户名
// 用户信息类, 包含账号和密码信息
SysUser sysUser = null;
try{
//根据传入用户名去数据库查找用户信息, 密码不能直接存在数据库, 要经过加密, 常用加密类为: BCryptPasswordEncoder
sysUser = sysUserService.getSysUserByUsename(username);
}catch (EmptyResultDataAccessException exception) { //没有对应的用户名异常
//登录各种失败采取抛错误交由MyAuthenticationFailureHandlertongyi8处理
throw new UsernameNotFoundException("用户或密码错误");
}
if(null == sysUser) {
throw new UsernameNotFoundException("用户名或密码错误");
}else {
/**
* org.springframework.security.core.userdetails.User
* User第一参数是:用户名
* 第二个参数是:pssword, 是从数据库查出来的
* 第三个参数是: 权限
*/
User user = null;
try{
user = new User(username,
sysUser.getPassword(),
// 配置用户访问权限
Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));
}catch (InternalAuthenticationServiceException exception) {
throw exception; // 在此处,将异常接着往外抛,抛给AuthenticationFailureHandler处理
}
return user;
}
}
}
用户数据表
create table sys_user(
id int primary key,
username char(20),
password char(100)
) ENGINE = MyIsam;
1.3 登录成功处理
要实现用户登录成功处理,需要实现AuthenticationSuccessHandler这个接口,然后实现接口中的方法:
//登录成功处理
@Component
public class MySuccessAuthenticationHandler implements AuthenticationSuccessHandler {
//该bean是springmvc启动的时候实例化的一个对象,纳入到容器中
@Autowired
private ObjectMapper objectMapper;
//登录成功执行的方法
//authentication中包含了用户的各种信息,包括UserDetail信息
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("code", 1);
map.put("msg", "登录成功");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(map)); //返回json数据
}
}
1.4 登录失败处理
// 失败处理
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
// 用户名或者秘密错误,在UserDetailsService中,将异常已经抛过来了
if(exception instanceof InternalAuthenticationServiceException) {
map.put("code", -2);
map.put("msg", "内部认证异常");
}else if(exception instanceof BadCredentialsException) {
map.put("code", -1);
map.put("msg", exception.getMessage());
}
response.setContentType("applicatoin/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(map)); //返回json数据
}
}
1.5 密码加密校验方式
@Component
public class CustomizePasswordEncoder implements PasswordEncoder {
// 注册的时候使用, 人为的去调用
@Override
public String encode(CharSequence rawPassword) {
return null;
}
// 当在返回UserDetails,会自动的去实现校验
// 第一个参数是传入的密码, 第二个参数是数据库查的密码,
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return false;
}
}
常用的是框架自带的BCryptPasswordEncoder()
- BCryptPasswordEncoder()源码
public class BCryptPasswordEncoder implements PasswordEncoder {
private Pattern BCRYPT_PATTERN = Pattern
.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
private final Log logger = LogFactory.getLog(getClass());
private final int strength;
private final SecureRandom random;
public BCryptPasswordEncoder() {
this(-1);
}
/**
* @param strength the log rounds to use, between 4 and 31
*/
public BCryptPasswordEncoder(int strength) {
this(strength, null);
}
/**
* @param strength the log rounds to use, between 4 and 31
* @param random the secure random instance to use
*
*/
public BCryptPasswordEncoder(int strength, SecureRandom random) {
if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
throw new IllegalArgumentException("Bad strength");
}
this.strength = strength;
this.random = random;
}
public String encode(CharSequence rawPassword) {
String salt;
if (strength > 0) {
if (random != null) {
salt = BCrypt.gensalt(strength, random);
}
else {
salt = BCrypt.gensalt(strength);
}
}
else {
salt = BCrypt.gensalt();
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.length() == 0) {
logger.warn("Empty encoded password");
return false;
}
if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
}
1.6. 记住我
1.6.1 基本原理
1.6.2 功能实现只需配置几个参数
后面有工程的整体配置, 当前配置仅供参考
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 查询用户数据
@Autowired
private UserSecurityService userSecurityService;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
jdbcTokenRepository.setCreateTableOnStartup(true); //true, 自动创建表,
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 意思是将图片验证码过滤器,加载用户名密码验证过滤器之前
http.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin() //使用form进行登录
.loginPage("/login.html") //指定登录页面
.loginProcessingUrl("/authentication/form") //表示form往哪里进行提
//记住我配置
.rememberMe()
.tokenValiditySeconds(36000000)
.tokenRepository(persistentTokenRepository())
.userDetailsService(userSecurityService) // 因为用户传入过来的token, 需要再次进行校验
.alwaysRemember(true)
}
}
1.7 . 图片验证码
1.8 封装验证码类
public class ImageCode implements Serializable{
private BufferedImage bufferedImage;
// code是随机字母,需要存储在session中
private String code;
// 过期时间
private LocalDateTime expireTime;
// 第三个参数为过期的时间
public ImageCode(BufferedImage bufferedImage, String code, int seconds) {
this.bufferedImage = bufferedImage;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(seconds); //设置过期的时间点
}
// 验证码是否过期
public boolean isExpire() {
return LocalDateTime.now().isAfter(expireTime); //当前时间是否在过期时间之后
}
// setters、getters、other constructors
}
1.9 controller层: 获取验证码图片
@Controller
@RequestMapping("/validate/code")
public class ValidataCodeController {
// Session的工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
public static final String VALIDATE_CODE_KEY = "VALIDATE_CODE_KEY";
@RequestMapping
public void validateCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = generate(); //生成验证码
// 将ImageCode存入到session
sessionStrategy.setAttribute(new ServletWebRequest(request), VALIDATE_CODE_KEY, imageCode);
//将图片写入前端
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
//生成图片验证码
private ImageCode generate() {
int width = 67;
int height = 30;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 随机生成文字
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 22);
}
g.dispose();
return new ImageCode(sRand, image, 60);
}
/**
* 生成随机背景条纹
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
1.10 增加图片验证码过滤器
/**
* 一次性的过滤器,过滤之前走,过滤之后不走了
*/
@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if(request.getMethod().equals("POST") && "/authentication/form".equals(request.getRequestURI())) {
try{
validateImageCode(request);
}catch (InternalAuthenticationServiceException exception) {
myAuthenticationFailureHandler.onAuthenticationFailure(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}else {
filterChain.doFilter(request, response);
}
}
// 校验验证码
private void validateImageCode(HttpServletRequest request) {
String validateCode = request.getParameter("validateCode"); //获取验证码
// 取到验证码
ImageCode imageCode = (ImageCode)sessionStrategy.getAttribute(new ServletWebRequest(request),
ValidataCodeController.VALIDATE_CODE_KEY);
// 验证码不能为空或者
if(StringUtils.isEmpty(validateCode) || "".equals(validateCode.trim())) {
throw new InternalAuthenticationServiceException("验证不能为空.");
}
if(imageCode.isExpire()) { //是否过期
throw new InternalAuthenticationServiceException("验证码过期.");
}
if(null == imageCode) {
throw new InternalAuthenticationServiceException("验证码不存在.");
}
if(!validateCode.equals(imageCode.getCode())) {
throw new InternalAuthenticationServiceException("验证码不正确");
}
}
}
1.11 全局安全配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 该bean的作用是,在UserDetailsService接口的loadUserByUsername返回的UserDetail中包含了
* password, 该bean就将用户从页面提交过来的密码进行处理,处理之后与UserDetail中密码进行比较。
* @return
*/
// 加载密码加密方法
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 登录成功执行方法
@Autowired
private MySuccessAuthenticationHandler mySuccessAuthenticationHandler;
//登录失败执行方法
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
//数据库连接池
@Autowired
private DataSource dataSource;
//自定义用户认证
@Autowired
private UserSecurityService userSecurityService;
//验证码过滤器
@Autowired
private ImageCodeValidateFilter imageCodeValidateFilter;
//token仓库
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
jdbcTokenRepository.setCreateTableOnStartup(true); //true, 自动创建表,
return jdbcTokenRepository;
}
//全局配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 意思是将图片验证码过滤器,加载用户名密码验证过滤器之前
http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class) //
.formLogin() //使用form进行登录
.loginPage("/login.html") //指定登录页面, 前后端分离不用指定
.loginProcessingUrl("/authentication/form") //表示form往哪里进行提交
.successHandler(mySuccessAuthenticationHandler) //成功后的处理
.failureHandler(myAuthenticationFailureHandler) //失败处理
.and()
.rememberMe() //记住我
.tokenValiditySeconds(36000000) //秒
.tokenRepository(persistentTokenRepository()) //指定token仓库
.userDetailsService(userSecurityService) // 因为用户传入过来的token, 需要再次进行校验
.alwaysRemember(true) //一直记住我
.and() //表示进行其他的配置
.authorizeRequests() //表示所有的都需要认证
.antMatchers("/login.html", "/js/**", "/css/**", "/validate/code").permitAll() //意思是让登录页面直接过
.anyRequest() // 对于所有的请求
.authenticated()
.and()
.csrf().disable(); //
}
//此方法用来获取密码令牌进行测试
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
System.out.println(bCryptPasswordEncoder.encode("1"));
}
}
如果上述jdbcTokenRepository.setCreateTableOnStartup(false); 需要手动建表,建表语句:
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null);