<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
加入 spring-boot-starter-security 依赖之后,不需要任何配置,启动项目,就会出现一个登录界面:
任何访问请求都会被拦截到 /login,并且要求认证后才能访问,请求方式是 Get
默认 username 是 user,每次启动都会在控制台输出一个 128Byte 的 password
可通过配置文件修改默认的 username 和 password (静态用户,适用于内部网络认证)
spring:
# default login url: /login
security:
user:
name: dev
# default size: 128 Byte
password: admin
@Data
public class User {
private Integer id;
private String name;
private String password;
}
配置文件:
# application name
spring:
# mysql
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spbt?serverTimezone=Asia/Shanghai
username: root
password: admin
# web service port
server:
port: 8088
mybatis-plus:
type-aliases-package: com.chenjy.security_demo.dto
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper:
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.chenjy.security_demo.mapper.UserMapper">
<select id="getUserByName" parameterType="string" resultType="user">
select
id,
name,
password
from
user
<where>
<if test="name != null and name != ''">
name = #{name}
if>
where>
select>
mapper>
@Mapper
public interface UserMapper {
User getUserByName(String name);
List<User> getUserByName();
}
启动类加上 @MapperScan 注解
service:
public interface UserService {
User getUserByName(String name);
List<User> getUserByName();
}
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public User getUserByName(String name) {
return userMapper.getUserByName(name);
}
@Override
public List<User> getUserByName() {
return userMapper.getUserByName();
}
}
要自定义 security 认证逻辑,就需要定义一个服务对象来实现 UserDetailsService 接口,并重写 loadUserByUsername 方法。
loadUserByUsername
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.chenjy.security_demo.dto.User user = userService.getUserByName(username);
if (user == null) {
throw new UsernameNotFoundException("用户名错误");
}
// 匹配用户密码
// org.springframework.security.core.userdetails.User
User res = new User(username, user.getPassword(), AuthorityUtils.createAuthorityList());
return res;
}
}
loadUserByUsername 需要返回一个 UserDetails 的实现类,可以直接使用 User,其有两个构造器:
public User(String username, String password, Collection<? extends GrantedAuthority> authorities)
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities)
security 内部会自动进行密码匹配
这时候直接启动项目,会报错:
*java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"*
这是因为 security 5.X 需要提供一个PasswordEncorder的实例 。
security 内部进行密码匹配的时候,一定要进行加密和解密处理。要求 IOC 容器中,必须要存在一个 PasswordEncorder 对象,提供加密和解密逻辑。
因为我的数据库密码并未进行加密,所以这里,我们就直接返回字符串进行明文比对就行了。
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/**
* @Description 收集页面的密码
* @param rawPassword
* @return String
*/
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
/**
* @Description 匹配逻辑
* @param rawPassword 明文,页面收集的密码
* @param encodedPassword 密文,存储在数据源中的密码
* @return boolean
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword);
}
}
encode 对页面收集到的密码进行加密,然后将加密好的密文交给 matches,与数据库密码进行比对。
自定义加密工具类
public class PwdEncode {
/**
* @Description 收集
* @param pwd
* @return String
*/
public static String encode(String pwd) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}
char[] charArray = pwd.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++) {
byteArray[i] = (byte) charArray[i];
}
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16){
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
/**
* @Description 解密
* 一次加密之后,需要两次解密
* @param pwd
* @return String
*/
public static String decrypt(String pwd) {
char[] a = pwd.toCharArray();
for (int i = 0; i < a.length; i++) {
a[i] = (char) (a[i] ^ 't');
}
String s = new String(a);
return s;
}
public static void main(String[] args) {
String s = "123456";
System.out.println("原始:" + s);
System.out.println("MD5后:" + encode(s));
System.out.println("加密的:" + decrypt(s));
System.out.println("解密的:" + decrypt(decrypt(s)));
}
}
然后修改数据库密码。
@Component
public class MyPasswordEncoder implements PasswordEncoder {
/**
* @Description 收集页面的密码
* @param rawPassword
* @return String
*/
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
/**
* @Description 匹配逻辑
* @param rawPassword 明文,页面收集的密码
* @param encodedPassword 密文,存储在数据源中的密码
* @return boolean
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return PwdEncode.encode(rawPassword.toString()).equals(encodedPassword);
}
}
注意: MD5解密很简单——https://www.cmd5.com/,所以不推荐使用MD5加密。
鉴于MD5加密的不安全性,所以建议使用 security 自带的加密工具类 —— BCryptPasswordEncoder 。
同一明文,加密两次,输出不同
public static void main(String[] args) {
String s = "123456";
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();;
System.out.println("原始: " + s);
System.out.println("加密1: " + encoder.encode(s));
System.out.println("加密1: " + encoder.encode(s));
}
可使用 matches 验证明文与密文是否相同:
System.out.println(encoder.matches(s, encoder.encode(s)));
BCryptPasswordEncoder使用bcrypt算法对密码进行加密,同时会为密码加上“盐”,开发者不需要自己加“盐”,即使相同的明文字段生成的加密字符串也不同。匹配时,从密文中取出“盐”,用该盐值加密明文和最终密文作对比。
BCryptPasswordEncoder的默认强度为10,开发者可以根据自己服务器的速度进行调整,以确保密码验证的时间约为1秒(官方建议)
BCryptPasswordEncoder encoder_pro = new BCryptPasswordEncoder(15);
System.out.println("加密2: " + encoder_pro.encode(s));
注册 BCryptPasswordEncoder:
取消MyPasswordEncoder的 @Component 注解
新建一个配置类,注册 BCryptPasswordEncoder
@Configuration
public class SecurityConf {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
resources/templates/login.html:
DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录title>
head>
<body>
<div class="main">
<div class="title">
<span>密码登录span>
div>
<div class="title-msg">
<span>请输入登录账户和密码span>
div>
<form class="login-form" method="post" novalidate th:action="@{/myLogin}">
<div class="input-content">
<div>
<input type="text"
autocomplete="off"
placeholder="用户名"
name="name"
required/>
div>
<div style="margin-top: 16px">
<input type="password"
autocomplete="off"
placeholder="登录密码"
name="password"
required
maxlength="32"/>
div>
div>
<div style="text-align: center">
<button type="submit" class="enter-btn" >登录button>
div>
form>
div>
body>
<style>
body{
background: #426258;
}
*{
padding: 0;
margin: 0;
}
.main {
padding-left: 25px;
padding-right: 25px;
padding-top: 15px;
width: 350px;
height: 350px;
background: #FFFFFF;
/*以下css用于让登录表单垂直居中在界面,可删除*/
position: absolute;
top: 50%;
left: 50%;
margin: -175px auto 0 -175px;
}
.title {
width: 100%;
height: 40px;
line-height: 40px;
}
.title span {
font-size: 18px;
color: #353f42;
}
.title-msg {
width: 100%;
height: 64px;
line-height: 64px;
}
.title:hover{
cursor: default ;
}
.title-msg:hover{
cursor: default ;
}
.title-msg span {
font-size: 12px;
color: #707472;
}
.input-content {
width: 100%;
height: 120px;
}
.input-content input {
width: 330px;
height: 40px;
border: 1px solid #dad9d6;
background: #ffffff;
padding-left: 10px;
padding-right: 10px;
}
.enter-btn {
width: 350px;
height: 40px;
color: #fff;
background: #0bc5de;
line-height: 40px;
text-align: center;
border: 0px;
}
.enter-btn:hover {
cursor:pointer;
background: #1db5c9;
}
style>
html>
注意: security 处理登录请求的方式一定是 POST 。
resources/templates/main.html:
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>主页title>
head>
<body>
<div class="con">
<h1><span style="color: #67865d">登陆成功span>h1>
<br>
<div class="component">
div>
<br>
div>
body>
<style>
body {
background-color: #c1d3d0;
}
.con {
text-align: center;
}
.component div {
font-size: 32px;
}
.component span {
color: #FFFFFF;
font-size: 32px;
}
style>
extends WebSecurityConfigurerAdapter 并重写 configure(HttpSecurity http) 。
@Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {
/**
* @Description 注册BCryptPasswordEncoder到容器
* @return BCryptPasswordEncoder
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* @Description 重写父类型中的配置逻辑
* @param http 基于http协议的security配置对象,包含所有security相关配置逻辑
* @throws Exception 配置出错会抛出异常
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置请求相关内容
http
.formLogin()
.loginPage("/myLogin") // 登录页面地址,默认 /login, 这里的地址要由 controller 接口地址决定
.usernameParameter("name") // 登录页面用户名字段
.passwordParameter("password"); // 登录页面用户密码字段
// 关闭csrf
http.csrf().disable();
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置请求相关内容
http
.formLogin()
.loginPage("/myLogin") // 登录页面地址,默认 /login, 这里的地址要由 controller 接口地址决定
.usernameParameter("name") // 登录页面用户名字段
.passwordParameter("password") // 登录页面用户密码字段
.and()
.authorizeRequests()
.antMatchers("/myLogin")
.permitAll() // /myLogin 不需要认证,可直接访问
.anyRequest()
.authenticated(); // 其他请求都需要认证
// 关闭csrf
http.csrf().disable();
}
注意:
security 默认访问成功后跳转到 “/” ,现在修改配置,使其跳转到 main。
方案一: 使用 successForwardUrl,请求转发 —— POST 请求。
@RequestMapping("main")
public String main() {
return "main";
}
.successForwardUrl("/main") // 登录成功跳转页面
方案二: 使用 successForwardUrl,响应重定向(重新请求) —— GET 请求。
.defaultSuccessUrl("http://127.0.0.1:8088/main") // 响应重定向(GET)
注意1: 因为 successForwardUrl 是重新发起请求,所以需要传入一个绝对地址才能成功定向。
注意2: 最好传入一个参数 alwaysUse (代表一定使用这个重定向地址),防止出现错误。
.defaultSuccessUrl("http://127.0.0.1:8088/main", true)
方案三: 使用 successHandler,自定义认证成功后的请求处理逻辑(转发、重定向都可自定义)。
successForwardUrl 和 defaultSuccessUrl 内部就是用的 successHandler 的实现类,进行控制成功后交给哪个方法处理。
自定义请求成功处理器: implements AuthenticationSuccessHandler
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SuccessHandler implements AuthenticationSuccessHandler {
private String url;
public boolean isRedirect;
/**
* @Description
* @param request 请求对象 请求转发——request.getRequestDispatcher(url).forward(request, response);
* @param response 响应对象 重定向——response.sendRedirect(url);
* @param authentication 认证成功后的对象,包含用户名和权限信息(防止信息泄露,所以没带密码)
*/
@Override
public void onAuthenticationSuccess( HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (isRedirect) {
response.sendRedirect(url);
} else {
request.getRequestDispatcher(url).forward(request, response);
}
}
}
.successHandler(new SuccessHandler("/main", false)) // 使用自定义请求处理控制逻辑
登陆失败后,默认跳转到 /login?error,现在自定义一个登录失败的处理。
方案一: 使用 failureForwardUrl,登录失败请求转发 —— POST。
@RequestMapping("fail")
public String fail(Model model) {
model.addAttribute("fail");
return "login";
}
.failureForwardUrl("/fail") // 登录失败转发
<span th:if="${fail} == 'true'" style="color: red">登陆失败span>
方案二: 使用 failureForwardUrl,响应重定向。
.failureUrl("/fail") // 响应重定向
...
.antMatchers("/myLogin", "/fail")
.permitAll() // /myLogin,/fail 不需要认证,可直接访问
重定向是重新发请求, 所以需要在权限配置中放行 /fail 。
方案三: 使用 failureForwardUrl,自定义处理器。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FailureHandler implements AuthenticationFailureHandler {
private String url;
private boolean isRedirect;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (isRedirect) {
response.sendRedirect(url);
} else {
request.getRequestDispatcher(url).forward(request, response);
}
}
}
.failureHandler(new FailureHandler("/fail", true)) //