一个小demo
-
准备环境
- maven、thymleaf、spring-security
- 对应的pom文件
-
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> </parent> <groupId>com.ay</groupId> <artifactId>spring-secruity-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-security-demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <thymeleafspringsecurity5.version> 3.0.3.RELEASE </thymeleafspringsecurity5.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>${thymeleafspringsecurity5.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <finalName>spring-security-demo</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> </plugins> </build> </project>
编写config文件
重写config方法,配置访问路径可以跳过认证授权,配置用户访问路径并且拥有根据拥有的角色判断是否放行;重写userDetailsService方法,将用户信息存入内存中;
对应的config代码:
// 开启 spring security,spring自动配置spring security
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 配置可允许的http请求
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 可允许的http url
.authorizeRequests()
// 以ant模式匹配的url匹配器,匹配一个或者多个目录或字符
// 允许静态资源
.antMatchers("/css/**", "/index").permitAll()
// 允许带user角色的
.antMatchers("/user/**").hasRole("USER")
// 回到security
.and()
// 设置通过表单进行登录验证
.formLogin()
// 登录页名称,不需要授权
.loginPage("/login")
// 校验的url地址,点击登录按钮时会跳转到该页面
.loginProcessingUrl("/loginUrl")
// 登录错误页面地址
.failureUrl("/login-error");
}
// 我们只需要定义用户信息存储的位置(示例中的UserDetailsService),
// 登录时Spring Security就可以自动帮我们完成用户认证工作。
// 把不同的请求分配给用户之后(示例中的SecurityConfig.configure()方法),
// 用户请求后台时Spring Security就可以自动完成对于请求的访问控制工作,
// 判断当前登录用户有没有权限访问这个路径。
@Override
@Bean
protected UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
// 用户名
.username("user")
// 密码
.password("password")
// 角色
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
编写controller文件
对应的代码:
@Controller
public class MainController {
@RequestMapping("/")
public String root() {
// 重定向到/index
return "redirect:/index";
}
@RequestMapping("/index")
public String index() {
// 返回index.html页面
return "index";
}
@RequestMapping("/user/index")
public String userIndex() {
// 返回/user/index.html页面
return "user/index";
}
@RequestMapping("/login")
public String login() {
// 跳转到登录页(login.html)
return "login";
}
@RequestMapping("/login-error")
public String loginError(Model model, String msg) {
if (msg == null) {
msg = "用户名或密码错误";
}
model.addAttribute("loginErrorMsg", msg);
// 跳转到登录页
return "login";
}
}
配置propeities文件
server.port=8080
# 设置session过期时间
#server.servlet.session.timeout=1m
logging.level.root=WARN
logging.level.org.springframework.web=INFO
logging.level.org.springframework.security=INFO
spring.thymeleaf.cache=false
# 错误页面相关配置
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=always
server.error.include-binding-errors=always
# 数据源
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## 这里填写你自己安装MySQL时设置的用户名
spring.datasource.username=
## 这里填写你自己安装MySQL时设置的密码
spring.datasource.password=
前端页面
对应的代码:
main.css
body {
font-family: sans;
font-size: 1em;
}
p.error {
font-weight: bold;
color: red;
}
templates->user->index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Security示例</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
</head>
<body>
<div th:substituteby="index::logout"></div>
<div style="background-color: lightgreen">
<h1>当前页面是安全的!</h1>
<p><a href="/index" th:href="@{/index}">返回主页</a></p>
</div>
</body>
</html>
templates->index.html login.html
index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3" lang="en">
<head>
<title>Spring Security示例</title>
<meta charset="utf-8" />
<link rel="stylesheet" th:href="@{/css/main.css}" />
</head>
<body>
<div th:fragment="logout" sec:authorize="isAuthenticated()" style="background-color: antiquewhite">
登录用户: <span sec:authentication="name"></span> |
角色: <span sec:authentication="principal.authorities"></span>
<div>
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="Logout" />
</form>
</div>
</div>
<div style="background-color: bisque">
<h1>Spring Security示例</h1>
<p>当前页面是不安全的,你可以通过登录之后访问安全的页面。</p>
<ul>
<li>访问<a th:href="@{/user/index}">安全的页面</a></li>
<li>访问<a th:href="@{/admin/api/hello}">访问/admin/api/hello</a></li>
<li>访问<a th:href="@{/user/api/hello}">访问/user/api/hello</a></li>
<li>访问<a th:href="@{/createUserPage}">创建用户页面</a></li>
</ul>
</div>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>登录页</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
<script type="application/javascript">
/*
* 点击验证码图片时刷新验证码
*/
function refreshCaptchaImg() {
document.getElementById('captchaImg').setAttribute('src', '/app/api/captcha?ts=' + Math.random());
}
</script>
</head>
<body>
<h1>登录页</h1>
<p>示例用户,用户名和密码分别为:user/password</p>
<!-- 登录失败时会显示该行内容-->
<p th:text="${loginErrorMsg}" class="error"></p>
<!-- logout后会显示该行内容-->
<p th:if="${param.logout}" class="error">已经退出。</p>
<form th:action="@{/loginUrl}" method="post">
<label for="username">用户名</label>:
<input type="text" id="username" name="username" autofocus="autofocus" /> <br />
<label for="password">密码</label>:
<input type="password" id="password" name="password" /> <br />
<!-- <label for="captcha">验证码</label> -->
<!-- <input type="text" id="captcha" name="captcha" placeholder="点击图片刷新" autocomplete="off"/> -->
<!-- <img id="captchaImg" src="/app/api/captcha" alt="验证码" onclick="refreshCaptchaImg()"/> <br /> -->
<!-- <input type="checkbox" name="remember-me">记住我 -->
<input type="submit" value="登录" />
</form>
<p>
<a href="/index" th:href="@{/index}">返回主页</a>
</p>
</body>
</html>
小总结:我们只需要定义用户信息存储的位置(示例中的UserDetailsService
),登录时Spring Security就可以自动帮我们完成用户认证工作。把不同的请求分配给用户之后(示例中的SecurityConfig.configure()
方法),用户请求后台时Spring Security就可以自动完成对于请求的访问控制工作,判断当前登录用户有没有权限访问这个路径。
认证和授权
最常用的一个模式:RBAC模式,即用户、角色、权限;通过给角色分配权限,给用户分配角色,就可以很好的控制每个用户应该拥有什么角色;
-
假设你要在一个APP上买东西:
- 用户(User):就是这个APP的使用者,也就是你自己。
- 角色(Role):对于APP而言,你的角色就是普通用户。APP一般都有后台管理系统,可以对你的订单进行修改、取消等操作。后台用户的角色则就是管理员。
- 权限(Permission):访问权限,比如你如果不登录APP,那么就不能把商品添加到购物车。这就表示未登录用户没有访问”添加商品到购物车“的权限。
- 整个认证授权流程如下:
-
用户首先需要登录系统。在APP的登录页面,输入自己的用户名和密码(有时候还需要输入验证码,还可以勾选”记住我“选项)。然后点击登录按钮,浏览器触发一个请求,请求到我们的后台系统。
-
后台系统的代码中进行登录处理。
1)如果数据库中不存在匹配的用户,则直接返回登录失败,告诉用户“用户名或密码错误”。
2)如果存在,则把用户的角色和权限从数据库中查询出来,放到缓存里,每个用户缓存信息都会有一个ID,叫做sessionID。然后把sessionID返回给客户端,用户之后的每次请求都会自动带着这个sessionID,这样我们就可以知道用户已经登录了,而且不需要再次查询数据库就能知道用户拥有哪些权限。
-
用户查看自己的订单。浏览器会自动把sessionID附带到这个请求信息里。
-
后台系统的代码中进行处理。
1)如果根据sessionID不能查到对应的缓存信息,表示缓存已经失效了,可能是用户长时间未操作,也可能是用户已经退出登录了。这时用户需要重新登录。
2)如果能查到缓存信息,则判断用户是否有查看订单的权限。如果没有,则告诉用户“权限不足”。
3)从数据库中查询属于该用户的订单,返回给APP进行展示。
-
实现demo security定义的数据库
添加数据库相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
配置数据库
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security?characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## 这里填写你自己安装MySQL时设置的用户名
spring.datasource.username=
## 这里填写你自己安装MySQL时设置的密码
spring.datasource.password=
创建数据库
数据库的语句在spring-security中保存,Spring Security将表结构定义在org/springframework/security/core/userdetails/jdbc/users.ddl文件(在Intellij IDEA中按Ctrl+Shift+N搜索users.ddl即可看到)中,把语句中的varchar_ignorecase
都替换为varchar
,替换后语句如下:
create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
修改配置文件
注意:这个demo使用的是security自定义的数据库,上一个demo将数据存放在内存中;所以配置user信息得使用JdbcUserDetailsManager;页面使用第一个demo的就可以,不过得在index(注意不是user下的index)添加:<li>访问<a th:href="@{/app/api/hello}">访问/app/api/hello</a></li>(在ul标签下)代码如下:
// 将用户信息存储在security自定义的数据库中
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/loginUrl")
.failureUrl("/login-error");
}
@Bean
protected UserDetailsService userDetailsService(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// 避免每次系统启动都创建用户
if (!manager.userExists("admin")) {
// 将创建的用户存入数据库
manager.createUser(User.withUsername("admin")
.password("{noop}password").roles("ADMIN", "USER").build());
}
if (!manager.userExists("user")) {
manager.createUser(User.withUsername("user").password("{noop}password").roles("USER").build());
}
return manager;
}
启动项目就可以访问了,启动之后security会自动给数据库插入数据,其中使用user访问admin的路径资源时会给出403的权限不足提示;
自定义数据库实现认证
引入mybatis依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
并在启动类添加mapperscan注解:@MapperScan("com.ay.springsecruitydemo.mapper")
创建数据库表并插入数据
-- 用户表
CREATE TABLE `spring_security`.`t_user` (
`id` bigint unsigned NOT NULL COMMENT '主键',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
`enable` bit(1) NULL DEFAULT NULL COMMENT '用户是否可用',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- 角色表
CREATE TABLE `spring_security`.`t_role` (
`id` bigint unsigned NOT NULL COMMENT '主键',
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色名称',
`code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色编码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- 用户角色表
CREATE TABLE `spring_security`.`t_user_role` (
`id` bigint unsigned NOT NULL COMMENT '主键',
`user_id` bigint(0) NULL DEFAULT NULL COMMENT '用户id',
`role_id` bigint(0) NULL DEFAULT NULL COMMENT '角色id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO t_user(username, `password`, `enable`) VALUES("admin", "{noop}password", 1);
INSERT INTO t_user(username, `password`, `enable`) VALUES("user", "{noop}password", 1);
INSERT INTO t_role(name, code) VALUES("普通用户", "ROLE_USER");
INSERT INTO t_role(name, code) VALUES("管理员", "ROLE_ADMIN");
-- 用户admin拥有ADMIN和USER两个角色
INSERT INTO t_user_role(user_id, role_id) VALUES(1, 1);
INSERT INTO t_user_role(user_id, role_id) VALUES(1, 2);
-- 用户user拥有USER一个角色
INSERT INTO t_user_role(user_id, role_id) VALUES(2, 1);
使用自定义数据库模型实现认证和授权需要自定义一个UserDetailsService
的实现类,该接口只有一个方法loadUserByUsername(String username)
,用于根据用户名查询一个包含了用户密码、权限等信息的UserDetails
对象。Spring Security会根据这些信息来判断认证是否成功。
添加lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
并在idea添加lombok插件
创建实体类对象,user、role
@Data
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private Boolean enable;
/** 用户拥有的权限 */
private List<GrantedAuthority> authorities;
@Override
public boolean isEnabled() {
return enable;
}
/** 以下方法暂时用不到,全部设置为true */
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
@Data
public class Role {
private Long id;
private String name;
private String code;
}
创建mapper
@Repository
public interface UserMapper {
/**
* 根据用户名查询用户
* @param username 用户名
* @return 用户
*/
@Select("select * from t_user where username = #{username}")
User loadUserByUsername(String username);
/**
* 根据用户id查询用户拥有的角色
* @param userId 用户id
* @return 用户拥有的角色
*/
@Select("SELECT r.* FROM t_user_role ur LEFT JOIN t_role r ON r.id = ur.role_id WHERE ur.user_id = #{userId}")
List<Role> loadRolesByUserId(Long userId);
}
config包下创建CustomUserDetailsServiceImpl
类
这个类就是我们自定义的UserDetailsService
实现。逻辑如下:
@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库中查询用户
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在!");
}
// 查询用户的角色
List<Role> roles = userMapper.loadRolesByUserId(user.getId());
// 把角色转换成数组
String[] roleCodes = roles.stream()
.map(Role::getCode)
.collect(Collectors.toList())
.toArray(new String[0]);
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(roleCodes);
// 设置用户的权限
user.setAuthorities(authorityList);
return user;
}
}
然后删除SecurityConfig
中的userDetailsService(DataSource dataSource)
方法,否则我们自定义的UserDetailsService
就不生效了。
实现验证码验证
引入验证码依赖
<!--生成验证码-->
<dependency>
<groupId>com.ramostear</groupId>
<artifactId>Happy-Captcha</artifactId>
<version>1.0.1</version>
</dependency>
处理验证码的请求
由于验证码谁都可以访问,所以放在app路径下:
@RestController
@RequestMapping("/app/api")
public class AppController {
@GetMapping("/hello")
public String hello() {
return "app公开请求";
}
@GetMapping("/captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {
HappyCaptcha.require(request, response)
// 表示生成中文算法类型的验证码
.type(CaptchaType.ARITHMETIC_ZH)
.build()
.finish();
}
}
在何处对验证码进行验证
因为验证码验证是发生在用户进行登录时进行的,但又不能是登录之后进行验证,所以就是在登录之前进行验证;基于Servlet,Filter会对每个客户端的请求进行拦截,如果校验通过就传递给下一个Filter
,否则就直接返回。直到所有Filter
都校验通过之后,客户请求才会到达我们定义的Controller
。因此我们可以自定义一个Filter
实现,然后插入到这个拦截链中,以此来执行验证码的校验。Spring Security拦截登录请求,执行登录处理的机制也是这样插入的,类名是UsernamePasswordAuthenticationFilter
。一般都是先校验验证码,再校验用户名和密码,因此我们可以把自定义的Filter
(类名定义为CaptchaFilter
)插入到UsernamePasswordAuthenticationFilter
的前面。配置方式是在SecurityConfig.configure(HttpSecurity http)
方法中添加最后两行代码:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 将用户信息存储在security自定义的数据库中
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/loginUrl")
.failureUrl("/login-error")
.and()
// 添加验证码过滤器在校验用户登录之前
.addFilterBefore(new CaptchaFilter(),
UsernamePasswordAuthenticationFilter.class);
}
}
自定义验证码实现
public class CaptchaFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 如果不是登录请求就不进行验证码校验
// request.getRequestURI()返回的是资源路径部分,不包括协议、主机名和端口号。
// request.getRequestURL()返回的是完整的URL,但不包括查询字符串,
// 它包含了协议、主机名、端口号(如果非默认)和资源路径。
if (!"/loginUrl".equals(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
// 获取输入的验证码
String captcha = request.getParameter("captcha");
boolean verification = HappyCaptcha.verification(request, captcha, true);
if (verification) {
// 验证通过,继续执行接下来的过滤链
filterChain.doFilter(request, response);
} else {
// 验证不通过,返回错误页面
response.sendRedirect("/login-error?msg=" + URLEncoder.encode("验证码不正确", "UTF-8"));
}
}
}
前端页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>登录页</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
<script type="application/javascript">
/*
* 点击验证码图片时刷新验证码
*/
function refreshCaptchaImg() {
document.getElementById('captchaImg').setAttribute('src', '/app/api/captcha?ts=' + Math.random());
}
</script>
</head>
<body>
<h1>登录页</h1>
<p>示例用户,用户名和密码分别为:user/password</p>
<!-- 登录失败时会显示该行内容-->
<p th:text="${loginErrorMsg}" class="error"></p>
<!-- logout后会显示该行内容-->
<p th:if="${param.logout}" class="error">已经退出。</p>
<form th:action="@{/loginUrl}" method="post">
<label for="username">用户名</label>:
<input type="text" id="username" name="username" autofocus="autofocus" /> <br />
<label for="password">密码</label>:
<input type="password" id="password" name="password" /> <br />
<label for="captcha">验证码</label>
<input type="text" id="captcha" name="captcha" placeholder="点击图片刷新" autocomplete="off"/>
<img id="captchaImg" src="/app/api/captcha" alt="验证码" onclick="refreshCaptchaImg()"/> <br />
<!-- <input type="checkbox" name="remember-me">记住我 -->
<input type="submit" value="登录" />
</form>
<p>
<a href="/index" th:href="@{/index}">返回主页</a>
</p>
</body>
</html>
实现自动登录
登录页面添加<input type="checkbox" name="remember-me">记住我
config方法添加
@Qualifier("customUserDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 配置要控制的http URL
.authorizeRequests()
// 对于静态文件和页面不拦截。
.antMatchers("/css/**", "/index").permitAll()
// /user/下的请求只有拥有USER角色的用户才能访问
.antMatchers("/user/**").hasRole("USER")
// USER角色的用户可以访问/user/api/下的资源
.antMatchers("/user/api/**").hasRole("USER")
// ADMIN角色的用户可以访问/admin/api/下的资源
.antMatchers("/admin/api/**").hasRole("ADMIN")
// /app/api/下的资源不做控制
.antMatchers("/app/api/**").permitAll()
// 回到HttpSecurity
.and()
// 设置通过表单进行登录认证
.formLogin()
// 登录页名称(即login.html),登录页不需要权限控制
.loginPage("/login")
// 登录校验地址,点击登录按钮时会跳转到该地址
.loginProcessingUrl("/loginUrl")
// 登录错误页地址
.failureUrl("/login-error")
.and()
// 添加验证码校验过滤器
.addFilterBefore(new CaptchaFilter(), UsernamePasswordAuthenticationFilter.class)
// 添加rememberMe配置
.rememberMe()
.userDetailsService(userDetailsService);
}
密码加密
编写前端页面
在user报下创建create.html页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Security示例</title>
<meta charset="utf-8"/>
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
</head>
<body>
<!-- action是点击确认时,实际保存用户的请求路径。method是请求的方式 -->
<form th:action="@{/admin/api/createUser}" method="post">
<label for="username">用户名:</label>
<input type="text" name="username" id="username"><br/>
<label for="password">密码:</label>
<input type="password" name="password" id="password"><br/>
<button type="reset" value="重置">重置</button>
<button type="submit" value="确认">确认</button>
</form>
</body>
</html>
在index.html页面下ul添加代码
<li>访问<a th:href="@{/createUserPage}">创建用户页面</a></li>
编写配置页面
在securityConfig类添加代码
// 加密密码
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
编写controller层代码
在MainController添加代码
@RequestMapping("/createUserPage")
public String createUserPage() {
return "user/create";
}
在AdminController添加代码
@Autowired
private UserService userService;
@PostMapping("/createUser")
public String createUser(User user) {
userService.insertUser(user);
return "成功";
}
编写Service层代码
创建service包,并在该包下创建impl包
添加UserService接口
public interface UserService {
void insertUser(User user);
}
在impl下添加实现类
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void insertUser(User user) {
User exitsUser = userMapper.loadUserByUsername(user.getUsername());
if (exitsUser != null) {
throw new RuntimeException("用户已存在");
}
// 密码加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
// 保存用户
userMapper.insertUser(user);
}
}
编写Mapper层
在UserMapper下添加代码
/**
* 保存用户
* @param user 用户
*/
@Insert("insert into t_user(username, password, enable) values (#{username}, #{password}, 1)")
void insertUser(User user);
注意:要设置id为自增主键
整体的包结构
运行代码即可
密码加密:开发人员使用自适应的单向加密函数来存储密码,这种加密函数会有意占用大量机器资源,可以配置工作因子,使资源消耗会随着硬件的改进而增加。例如把工作因子设置为1秒钟,这样即使攻击者使用超级计算机来暴力破解密码也需要耗费同样的时间。Spring Security的密码加密实现就推荐使用这样的函数。
CSRF防护
CSRF,即跨站请求伪造;盗用客户端的信息,请求服务端;CSRF的原理就是利用浏览器,诱骗用户点击伪造的链接,用户点击这个链接后,请求信息中就会附带用户的真实的Cookie信息。由于Cookie中包含了用户的会员ID,服务端是没办法区分这个请求是不是用户真实的意愿。
防护方法
- HTTP Referer
- HTTP Referer是HTTP请求头中的一个字段,用于标识请求的来源。例如,你要在百度查询一个内容,打开浏览器的控制台(快捷键F12),切换到Network选项,可以看到请求头中的Referer的值就是百度的网址。这个字段是浏览器自动添加的,如果是用户点击了伪造的链接发起的请求,那么该字段的值就会不是我们期望的网站,服务端就可以认为是CSRF攻击,直接拒绝访问。这种方式实现简单,但并不完全可靠。首先,对于某些浏览器,比如IE6,可以有一些方法篡改Referer的值。其次,Referer值会记录用户的访问来源,如果用户不想把这个信息泄漏出去,然后在浏览器中设置了发送请求时不提供Referer,那么用户的正常请求也会被认为是CSRF攻击。
- Token验证
- 如果能在请求中放入恶意用户不能伪造的信息,并且该信息不存在于Cookie中,这样服务端就能判断出来是不是CSRF攻击。具体的作法就是在用户登录的时候,服务端给客户端发放一个token,用户发起登录请求时,附带这个token值作为参数。登录之后,服务端会记录这个session的token。之后用户的每次请求都要附带这个token值,服务端会根据它来跟用户的session中的token比对。如果不同,则认为是CSRF攻击。
- Spring Security来防御CSRF攻击只需要在
SecurityConfig
的configure(HttpSecurity http)
方法的最后面添加以下代码就可以了.and() // 配置CSRF防护,使用默认配置 .csrf();
- 如果修改了这个token值,服务端会返回一个错误页面,状态是403。百度百科对于403的解释是“服务器理解客户的请求,但拒绝处理它”,这里的话其实就是认为这个请求是CSRF攻击,而拒绝处理它。这就是我们想要的效果。
扩展学习
- 单点登录(SSO)
- 你可能不知道单点登录是什么,但是你肯定享受过单点登录这项技术带来的便利。例如,你肯定有过这样的经历,当你在淘宝上购物时,有时候查看一个商品时,可能会发现跳转到了天猫的系统里(因为这个商品是天猫平台的)。如果你已经登录了淘宝系统,那么你不用登录便可以把这件天猫商城的商品添加到购物车里。这,你可能觉得习以为常,但是为什么两个不同的系统只需要登录一次就可以共享登录状态呢?其实这就用到了单点登录的技术。如果你也想让自己的不同的系统之间实现这样的功能,那你就需要了解一下怎么使用Spring Security和CAS框架来实现了。
- 集群会话管理
-
会话也就是前面说到的用户的session。因为HTTP是无状态的,也就是说你第二次访问系统时,系统并不知道你是第一次还是第n次访问,你的每个请求之间都没有关联。为了区分用户的身份,需要一种机制来保存用户的状态,会话就是出于这个目的而设计的。对于传统的单体应用而言,用户的会话一般保存在内存里。服务器内存是有限的,当登录用户数量过多时,应用就会因为内存不足而响应缓慢,甚至宕机。知道了这个你也就能理解为什么秒杀系统容易崩溃了。另外,在分布式系统中,因为用户的请求可能打到任何一台服务器上,所以每台服务器上都可能会存储同一个用户的会话,这样也是对于内存的一种浪费。而且用户即使已经在A服务器上登录了,当他的请求打到B服务器上时,由于B服务器上还没有他的会话,还会让用户去登录。所以现在的应用一般会把会话存储交给到一个单独的服务去处理,比如最常见的就是把用户的会话信息存储到Redis里。这样一来,存储用户会话的内存开销就不会影响应用本身了。这种方案称为session共享,Spring Session框架对于session的存储提供了多种类型的支持,它与Spring Security也能很方便的整合。如果你的系统面临会话管理的问题,那么你就需要去学习一下session共享和Spring Session了。
-
- OAuth
- OAuth(Open Authorization)翻译过来就是开放授权,这种授权机制可以在用户不向第三方应用提供密码的情况下,让第三方应用获取用户数据。听起来是不是很困惑?其实这个也是很常见的。现在很多互联网系统为了方便用户使用,都会不强制用户注册,而是可以通过QQ就可以登录它们的系统。比如这个网站。 用户省去了注册账号的时间,也不用担心QQ密码泄露,还能一个账号走天下,实在是太方便了。而在这一切的背后就是用到了OAuth这个技术。如果你想让你的系统也拥有这种可以通过QQ号登录的能力,那么你就需要了解下OAuth的大概原理以及实现。虽然QQ本身提供了接入的方法,但是接入过程相对比较繁琐,而Spring Social框架提供了快速实现这种机制的功能,并且它与Spring Security整合起来也是很方便的(毕竟都是一家人)。所以你可以试试Spring Social。
总结
- 在课程的一开始,讲了什么是安全与权限控制,介绍了Java应用比较流行的两款安全管理框架。
- 之后学习了Spring Security可以为我们做哪些事情(用户认证、密码加密、授权和防止漏洞),然后用一个简单的示例项目演示了Spring Security的引入方式和基本功能。
- 有了总体的了解之后,接下来主要理解生产级应用的认证流程是什么样的,重点是理解流程和掌握自定义数据库模型接入Spring Security的认证和授权。
- 之后跟登录相关,把登录页改造成生产级应用常见的登录页,添加了图形验证码和自动登录功能。这两个功能实现起来都比较简单,主要是理解它们的用途。
- 再之后主要是安全防护,安全防护是一种非常重要但是往往又很容易被开发者和企业忽视的一种功能。对它有所了解,不然等需要面对这些问题的时候往往会一头雾水。