本文通过一个登录的例子介绍 SpringBoot + Spring Security + Thymeleaf 权限管理。
用户登录账号是 admin,saysky,lockeduser
密码都是 123456
1、表结构
user 表
authority 表
user_authority 表
2、数据
user 表
authority 表
user_authority 表
3、SQL 代码
pom.xml
主要需要 SpringBoot、Thymeleaf、Spring Security 的依赖
User.java
Authority.java
UserRepository.java
CustomUserService.java
SecurityConfig.java
首页:http://localhost:8080
登录页面:http://localhost:8080/login
登录错误页面:http://localhost:8080/login?error=true
退出登录:http://localhost:8080/logout
index.html
login.html
1、访问首页:http://localhost:8080
2、点击 登录,填写登录信息,不勾选记住我
账号:admin,密码:123456
3、登录成功,跳转到首页,显示登录信息
4、访问 http://localhost:8080/logout,可退出登录
5、登录错误显示
这里,登录失败的话(用户名不存在或者密码错误),都是显示Bad credentials
如果登录被锁定的用户,如用户名为lockeduser
6、记住我也是有效的,关闭浏览器后,下次启动,依然是登录状态
记住密码后,我们可以看到浏览器里添加了 一条 cookie
本文地址:https://liuyanzhao.com/7431.html
一、数据库
用户登录账号是 admin,saysky,lockeduser
密码都是 123456
1、表结构
user 表
authority 表
user_authority 表
2、数据
user 表
authority 表
user_authority 表
3、SQL 代码
- SET NAMES utf8;
- SET FOREIGN_KEY_CHECKS = 0;
- -- ----------------------------
- -- Table structure for `authority`
- -- ----------------------------
- DROP TABLE IF EXISTS `authority`;
- CREATE TABLE `authority` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `name` varchar(255) NOT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
- -- ----------------------------
- -- Records of `authority`
- -- ----------------------------
- BEGIN;
- INSERT INTO `authority` VALUES ('1', 'ROLE_ADMIN'), ('2', 'ROLE_USER');
- COMMIT;
- -- ----------------------------
- -- Table structure for `user`
- -- ----------------------------
- DROP TABLE IF EXISTS `user`;
- CREATE TABLE `user` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `username` varchar(20) NOT NULL,
- `password` varchar(100) NOT NULL,
- `name` varchar(20) NOT NULL,
- `email` varchar(50) NOT NULL,
- `avatar` varchar(200) DEFAULT NULL,
- `create_time` datetime DEFAULT NULL,
- `last_login_time` datetime DEFAULT NULL,
- `status` varchar(10) DEFAULT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `UK_ob8kqyqqgmefl0aco34akdtpe` (`email`),
- UNIQUE KEY `UK_sb8bbouer5wak8vyiiy4pf2bx` (`username`)
- ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- -- ----------------------------
- -- Records of `user`
- -- ----------------------------
- BEGIN;
- INSERT INTO `user` VALUES ('1', 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '管理员', 'admin@liuyanzhao.com', null, null, null, 'normal'), ('2', 'saysky', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '言曌', '847064370@qq.com', null, null, null, 'normal'), ('3', 'lockuser', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '锁定账号', 'locked@qq.com', null, null, null, 'locked');
- COMMIT;
- -- ----------------------------
- -- Table structure for `user_authority`
- -- ----------------------------
- DROP TABLE IF EXISTS `user_authority`;
- CREATE TABLE `user_authority` (
- `user_id` bigint(20) NOT NULL,
- `authority_id` bigint(20) NOT NULL,
- KEY `FKgvxjs381k6f48d5d2yi11uh89` (`authority_id`),
- KEY `FKpqlsjpkybgos9w2svcri7j8xy` (`user_id`),
- CONSTRAINT `FKgvxjs381k6f48d5d2yi11uh89` FOREIGN KEY (`authority_id`) REFERENCES `authority` (`id`),
- CONSTRAINT `FKpqlsjpkybgos9w2svcri7j8xy` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- -- ----------------------------
- -- Records of `user_authority`
- -- ----------------------------
- BEGIN;
- INSERT INTO `user_authority` VALUES ('1', '1'), ('2', '2'), ('1', '2');
- COMMIT;
- SET FOREIGN_KEY_CHECKS = 1;
二、Maven 依赖
pom.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.liuyanzhao</groupId>
- <artifactId>chuyun</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <packaging>war</packaging>
- <name>chuyun</name>
- <description>Chuyun Blog for Spring Boot</description>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>1.5.9.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- <java.version>1.8</java.version>
- <thymeleaf.version>3.0.3.RELEASE</thymeleaf.version>
- <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version>
- <elasticsearch.version>2.4.4</elasticsearch.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!--lombok-->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- <!-- Thymeleaf -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- <!-- Spring Data JPA-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- <!-- mysql-->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!-- 热部署 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-devtools</artifactId>
- <optional>true</optional>
- </dependency>
- <!-- Spring Boot Elasticsearch 依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
- </dependency>
- <dependency>
- <groupId>net.java.dev.jna</groupId>
- <artifactId>jna</artifactId>
- <version>4.3.0</version>
- </dependency>
- <!-- Spring Security 依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
- <dependency>
- <groupId>org.thymeleaf.extras</groupId>
- <artifactId>thymeleaf-extras-springsecurity4</artifactId>
- <version>3.0.2.RELEASE</version>
- </dependency>
- <!--fasthson-->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.7</version>
- </dependency>
- <!--验证码kaptcha-->
- <dependency>
- <groupId>com.github.penggle</groupId>
- <artifactId>kaptcha</artifactId>
- <version>2.3.2</version>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <configuration>
- <fork>true</fork>
- </configuration>
- </plugin>
- </plugins>
- </build>
- </project>
主要需要 SpringBoot、Thymeleaf、Spring Security 的依赖
三、实体类
User.java
- package com.liuyanzhao.chuyun.entity;
- import lombok.Data;
- import org.hibernate.validator.constraints.Email;
- import org.hibernate.validator.constraints.NotEmpty;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.authority.SimpleGrantedAuthority;
- import org.springframework.security.core.userdetails.UserDetails;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- import javax.persistence.*;
- import javax.validation.constraints.Size;
- import java.io.Serializable;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Date;
- import java.util.List;
- /**
- * @author 言曌
- * @date 2017/12/28 上午9:06
- */
- @Entity // 实体
- @Data
- public class User implements UserDetails, Serializable {
- private static final long serialVersionUID = 1L;
- @Id // 主键
- @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
- private Long id; // 用户的唯一标识
- @NotEmpty(message = "昵称不能为空")
- @Size(min=2, max=20)
- @Column(nullable = false, length = 20) // 映射为字段,值不能为空
- private String name;
- @NotEmpty(message = "邮箱不能为空")
- @Size(max=50)
- @Email(message= "邮箱格式不对" )
- @Column(nullable = false, length = 50, unique = true)
- private String email;
- @NotEmpty(message = "账号不能为空")
- @Size(min=3, max=20)
- @Column(nullable = false, length = 20, unique = true)
- private String username; // 用户账号,用户登录时的唯一标识
- @NotEmpty(message = "密码不能为空")
- @Size(max=100)
- @Column(length = 100)
- private String password; // 登录时密码
- @Column(length = 200)
- private String avatar; // 头像图片地址
- private Date createTime;
- private Date lastLoginTime;
- @Column(length = 10)
- private String status;
- @ManyToMany(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
- @JoinTable(name = "user_authority", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
- inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id"))
- private List<Authority> authorities;
- protected User() { // JPA 的规范要求无参构造函数;设为 protected 防止直接使用
- }
- public User(String name, String email,String username,String password) {
- this.name = name;
- this.email = email;
- this.username = username;
- this.password = password;
- }
- public Collection<? extends GrantedAuthority> getAuthorities() {
- // 需将 List<Authority> 转成 List<SimpleGrantedAuthority>,否则前端拿不到角色列表名称
- List<SimpleGrantedAuthority> simpleAuthorities = new ArrayList<>();
- for(GrantedAuthority authority : this.authorities){
- simpleAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
- }
- return simpleAuthorities;
- }
- public void setAuthorities(List<Authority> authorities) {
- this.authorities = authorities;
- }
- public void setEncodePassword(String password) {
- PasswordEncoder encoder = new BCryptPasswordEncoder();
- String encodePasswd = encoder.encode(password);
- this.password = encodePasswd;
- }
- @Override
- public boolean isAccountNonExpired() {
- return true;
- }
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
- @Override
- public boolean isEnabled() {
- return true;
- }
- }
Authority.java
- package com.liuyanzhao.chuyun.entity;
- import org.springframework.security.core.GrantedAuthority;
- import javax.persistence.*;
- /**
- * 权限
- * @author 言曌
- * @date 2018/1/26 下午2:05
- */
- @Entity
- public class Authority implements GrantedAuthority {
- private static final long serialVersionUID = 1L;
- @Id // 主键
- @GeneratedValue(strategy = GenerationType.IDENTITY) // 自增长策略
- private Long id; // 用户的唯一标识
- @Column(nullable = false) // 映射为字段,值不能为空
- private String name;
- public Long getId() {
- return id;
- }
- public void setId(Long id) {
- this.id = id;
- }
- @Override
- public String getAuthority() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
四、Dao 层
UserRepository.java
- package com.liuyanzhao.chuyun.repository;
- import com.liuyanzhao.chuyun.entity.User;
- import org.springframework.data.jpa.repository.JpaRepository;
- /**
- * 用户repository
- * @author 言曌
- * @date 2017/12/27 0027 20:50
- */
- public interface UserRepository extends JpaRepository<User, Long> {
- /**
- * 根据用户名查找用户
- * @param username
- * @return
- */
- User findByUsername(String username);
- }
五、Service 层
CustomUserService.java
- package com.liuyanzhao.chuyun.service;
- import com.liuyanzhao.chuyun.entity.User;
- import com.liuyanzhao.chuyun.repository.UserRepository;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.authentication.LockedException;
- import org.springframework.security.core.userdetails.UserDetailsService;
- import org.springframework.security.core.userdetails.UsernameNotFoundException;
- import org.springframework.stereotype.Service;
- /**
- * @author 言曌
- * @date 2018/1/30 下午8:37
- */
- @Service
- public class CustomUserService implements UserDetailsService{
- @Autowired
- private UserRepository userRepository;
- @Override
- public User loadUserByUsername(String username) throws UsernameNotFoundException {
- User user = userRepository.findByUsername(username);
- if (user == null) {
- throw new UsernameNotFoundException("用户名不存在");
- } else if("locked".equals(user.getStatus())) { //被锁定,无法登录
- throw new LockedException("用户被锁定");
- }
- return user;
- }
- }
六、Spring Security 配置
SecurityConfig.java
- package com.liuyanzhao.chuyun.config;
- import com.liuyanzhao.chuyun.service.CustomUserService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.security.authentication.AuthenticationProvider;
- import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
- import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
- import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
- 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.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- /**
- * 安全配置类
- *
- * @author 言曌
- * @date 2018/1/23 上午11:37
- */
- @EnableWebSecurity
- @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法安全设置
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- private static final String KEY = "liuyanzhao.com";
- @Autowired
- private PasswordEncoder passwordEncoder;
- @Bean
- CustomUserService customUserService() {
- return new CustomUserService();
- }
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder(); // 使用 BCrypt 加密
- }
- @Bean
- public AuthenticationProvider authenticationProvider() {
- DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
- authenticationProvider.setUserDetailsService(customUserService());
- authenticationProvider.setPasswordEncoder(passwordEncoder); // 设置密码加密方式
- return authenticationProvider;
- }
- /**
- * 自定义配置
- */
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests().antMatchers("/","/css/**", "/js/**", "/fonts/**","/users").permitAll() // 都可以访问
- .antMatchers("/h2-console/**").permitAll() // 都可以访问
- .antMatchers("/admin/**").hasRole("ADMIN") // 需要相应的角色才能访问
- .antMatchers("/console/**").hasAnyRole("ADMIN","USER") // 需要相应的角色才能访问
- .and()
- .formLogin() //基于 Form 表单登录验证
- .loginPage("/login").failureUrl("/login?error=true") // 自定义登录界面
- .and().rememberMe().key(KEY) // 启用 remember me
- .and().exceptionHandling().accessDeniedPage("/403"); // 处理异常,拒绝访问就重定向到 403 页面
- http.csrf().ignoringAntMatchers("/h2-console/**"); // 禁用 H2 控制台的 CSRF 防护
- http.headers().frameOptions().sameOrigin(); // 允许来自同一来源的H2 控制台的请求
- }
- /**
- * 认证信息管理
- *
- * @param auth
- * @throws Exception
- */
- @Autowired
- public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
- //auth.userDetailsService(userDetailsService);
- auth.userDetailsService(customUserService());
- auth.authenticationProvider(authenticationProvider());
- }
- }
七、HTML 页面
首页:http://localhost:8080
登录页面:http://localhost:8080/login
登录错误页面:http://localhost:8080/login?error=true
退出登录:http://localhost:8080/logout
index.html
- <!DOCTYPE html>
- <html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:th="http://www.thymeleaf.org"
- xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport"
- content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
- </head>
- <body>
- <!--匿名-->
- <div sec:authorize="isAnonymous()">
- 未登录,点击 <a th:href="@{/login}">登录</a>
- </div>
- <!--登录-->
- <div sec:authorize="isAuthenticated()">
- <p>已登录</p>
- <p>登录名:<span sec:authentication="name"></span></p>
- <p>角色:<span sec:authentication="principal.authorities"></span></p>
- <p>Username:<span sec:authentication="principal.username"></span></p>
- <p>Password:<span sec:authentication="principal.password"></span></p>
- <p>Email :<span sec:authentication="principal.email"></span></p>
- <p>Name:<span sec:authentication="principal.name"></span></p>
- <p>Status:<span sec:authentication="principal.status"></span></p>
- <p>拥有的角色:
- <span sec:authorize="hasRole('ROLE_ADMIN')">管理员</span>
- <span sec:authorize="hasRole('ROLE_USER')">用户</span>
- </p>
- </div>
- </body>
- </html>
login.html
- <!DOCTYPE html>
- <html xmlns="http://www.w3.org/1999/xhtml"
- xmlns:th="http://www.thymeleaf.org">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
- <title>登录页面</title>
- <body>
- <form th:action="@{login}" method="post" id="loginForm">
- <span th:if="${param.error}" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
- 用户名:<input type="text" name="username" class="username" id="username" placeholder="用户名" autocomplete="off"/> <br>
- 密 码:<input type="password" name="password" class="password" id="password" placeholder="密码"
- oncontextmenu="return false"
- onpaste="return false"/> <br>
- <input type="checkbox" name="remember-me"/>记住我 <br>
- <input id="submit" type="submit" value="登录"/>
- </form>
- </body>
- </html>
八、运行效果
1、访问首页:http://localhost:8080
2、点击 登录,填写登录信息,不勾选记住我
账号:admin,密码:123456
3、登录成功,跳转到首页,显示登录信息
4、访问 http://localhost:8080/logout,可退出登录
5、登录错误显示
这里,登录失败的话(用户名不存在或者密码错误),都是显示Bad credentials
如果登录被锁定的用户,如用户名为lockeduser
6、记住我也是有效的,关闭浏览器后,下次启动,依然是登录状态
记住密码后,我们可以看到浏览器里添加了 一条 cookie
本文地址:https://liuyanzhao.com/7431.html
2020年11月09日 11:34:01
你好问一下我获取到的角色这样子怎么遍历啊:[cn.tju.authority.config.security.service.MyGrantedAuthority@655fdfd4, cn.tju.authority.config.security.service.MyGrantedAuthority@25ea95bf]
2019年07月15日 16:26:14
请问能贴一下application.properties吗
2019年01月24日 14:28:47
你controller层?代码不粘贴全就发出来?
2019年05月24日 12:53:54
这里的登陆验证不需要controller 用dao继承JPA 再写两个方法直接在继承了UserDetails的Service中调用就可以了 要什么controller
2021年11月23日 15:04:03
@1458598648:hello
2021年11月23日 15:04:31
@hello:word
2021年11月23日 15:05:00
@word:hello
2021年11月23日 15:05:21
@hello:word
2021年11月23日 15:07:21
@word:言曌
2021年11月23日 15:08:00
@言曌:这里的登陆验证不需要controller 用dao继承JPA 再写两个方法直接在继承了UserDetails的Service中调用就可以了 要什么controller
2019年01月24日 16:00:25
请问 controller 贴什么
2018年02月03日 21:39:41
支持一下!