目录
1,SpringSecurity介绍
SpringSecurity是一个安全访问控制框架,就是一个专门进行认证授权的安全框架。
1,认证
就是就是判断一个用户的身份是否合法的过程。
2,会话技术
1,session
- 用户登录成功,服务器端会为客户端创建一个session对象,分配sessionId,sessionId映射session对象的关系存储到session缓存,再将sessionId响应给客户端,客户端会将sessionId保存在它的cookie中。
- 客户端访问服务端归还sessionId,从session缓存中拿到对应的session对象
2,token(令牌)
- 用户登录服务器端生成一个token(字符串的id),响应到客户端,客户端会将token存储在cookie或localstorage中。
- 客户端访问服务端归还token,服务器端对token进行解析效验,通过及登录
- 三种方式:
- 服务器端向客户端颁发普通的token到redis
- 服务器端向客户端颁发jwt token
- 服务器端向客户端颁发jwt token,并存储在redis中
3,授权
判断用户权限,并使不同权限的用户跨域访问不同的资源
4,RBAC
5张表实现用户角色权限控制:
- 用户表:存储用户信息
- 角色表:存储角色信息
- 用户角色关系表:用户id和角色id相关联
- 权限表:权限内容,菜单
- 角色权限关系表:角色id和权限id相关联
5,接口概念
- interface:对外暴露的guize
- 系统资源url:url接口,api接口
2,Security的使用
1,引入Security的场景启动器的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2,认证登录
1,默认密码登录
账号:user
密码如下图:运行程序控制台输出的密码
可以测试访问
2,在配置文件里配置账户密码登录
只能配置一个
可以测试访问
3, 在Security配置类中配置
创建SpringSecurity的配置类
SecurityConfig继承父类WebSecurityConfigurerAdapter
重写void configure(AuthenticationManagerBuilder auth)方法
在内存中配置账户,密码,访问权限
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//模拟内存用户 -- 在内存中配置
auth.inMemoryAuthentication()
.withUser("admin")//用户名
.password("123")//密码
.authorities("user:add","user:delete","user:update","user:query")//权限
.and()
.withUser("zhangsan")
.password("123")
.authorities("user:add","user:query");
}
}
可以测试访问
3,配置加密器
修改SpringSecurity的配置类
.password(passwordEncoder().encode("123"))//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
//返回PasswordEncoder的实现BCryptPasswordEncoder的实例
return new BCryptPasswordEncoder();
}
可以测试访问
配置加密器,接口PasswordEncoder的实现类BCryptPasswordEncoder的对象
加密器对象 passwordEncoder的常用方法
passwordEncoder.encode(明文密码)------------返回String类型加密密码
passwordEncoder.matches(明文密码,加密密码)----------效验两个密码,返回值boolean
4,获取当前登录的用户信息
@RestController
public class HelloController {
@RequestMapping("/userInfo1")
public Principal userInfo1(Principal principal){
return principal;
}
@RequestMapping("/userInfo2")
public Object userInfo2(){
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
@RequestMapping("/userInfo3")
public Authentication userInfo3(Authentication authentication){
return authentication;
}
@RequestMapping("/userInfo4")
public Authentication userInfo4(){
return SecurityContextHolder.getContext().getAuthentication();
}
}
- 将Principal作为参数注入给请求处理方法,返回值Principal
- SecurityContextHolder.getContext().getAuthentication().getPrincipal(),返回值Object
- 获取封装了登录用户信息的Authentication对象
- 将Authentication作为参数注入给请求处理方法,返回值Authentication
- SecurityContextHolder.getContext().getAuthentication(),返回值Object
5,授权
1,用户、角色、权限配置方式
1,编写TestController
@RestController
public class HelloController2 {
@RequestMapping("/welcome")
public String welcome(){
return "welcome";
}
@RequestMapping("/add")
public String add(){
return "add";
}
@RequestMapping("/delete")
public String delete(){
return "delete";
}
@RequestMapping("/update")
public String update(){
return "update";
}
@RequestMapping("/query")
public String query(){
return "query";
}
@RequestMapping("/free")
public String free(){
return "free";
}
}
2,修改 SpringSecurity的配置类
重写void configure(HttpSecurity http)方法请求配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()//提供一个登录表单
.successForwardUrl("/welcome")//登录成功转发请求 /项目/welcome
.failureForwardUrl("/login");//登录失败转发请求 /项目/login,回到登录表单
/*
url请求授权:
*/
http.authorizeRequests()
//具有sys:add权限可发出/项目/add的请求
.antMatchers("/add").hasAnyAuthority("sys:add")
//具有sys:delete权限可发出/项目/delete的请求
.antMatchers("/delete").hasAnyAuthority("sys:delete")
//具有sys:update权限可发出/项目/update的请求
.antMatchers("/update").hasAnyAuthority("sys:update")
//具有sys:query权限可发出/项目/query的请求
.antMatchers("/query").hasAnyAuthority("sys:query");
http.authorizeRequests()
//对/项目/free的请求,不受任何限制,直接放行
.antMatchers("/free").permitAll();
http.authorizeRequests()
//其它所有请求,登录后就可以请求
.anyRequest().authenticated();
}
2, 方法级别的授权方式
1,修改SpringSecurity的配置类
//开启方法级别的授权
@EnableGlobalMethodSecurity(prePostEnabled = true)
2,修改SpringSecurity的配置类的请求配置
http.authorizeRequests()
//具有sys:add权限可发出/项目/add的请求
.antMatchers("/add").hasAnyAuthority("sys:add")
//具有sys:delete权限可发出/项目/delete的请求
.antMatchers("/delete").hasAnyAuthority("sys:delete")
//具有sys:update权限可发出/项目/update的请求
.antMatchers("/update").hasAnyAuthority("sys:update")
//具有sys:query权限可发出/项目/query的请求
.antMatchers("/query").hasAnyAuthority("sys:query");
3,或者修改TestController
在每个请求下加如下注解
@PreAuthorize("hasAnyAuthority('sys:add')")具有sys:add权限可发出
/项目/add的请求即执行该方法;
6,向客户端响应json数据
1,如果是前后端不分离的项目
在Security配置类中配置,重写void configure(HttpSecurity http)方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successForwardUrl("/welcome")//登录成功转发请求
.failureForwardUrl("/login");//登录失败转发请求
}
2, 如果是前后端分离的项目
1,自定义认证成功处理器
定义AuthenticationSuccessHandler接口的实现类并重写其抽象方法
onAuthenticationSuccess(HttpServletRequest,HttpServletResponse,Authentication),
在方法中完成登录成功后向客户端响应json,最后将其bean对象添加到IOC容器。
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse
response, Authentication authentication)
throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("code", 200);
map.put("msg", authentication.getPrincipal());//msg -- 用户信息
ObjectMapper mapper = new ObjectMapper();
String jsonStr = mapper.writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(jsonStr);
}
}
2, 自定义认证失败处理器
定义AuthenticationFailureHandler接口的实现类并重写其抽象方法
onAuthenticationFailure(HttpServletRequest,HttpServletResponse,AuthenticationException),
在方法中完成登录失败后向客户端响应json,最后将其bean对象添加到IOC容器。
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse
response, AuthenticationException exception)
throws IOException, ServletException {
Map<String,Object> map = new HashMap<>();
map.put("code", 401);
if (exception instanceof LockedException) {
map.put("msg", "登陆失败,账户被锁定!");
} else if (exception instanceof BadCredentialsException) {
map.put("msg", "登陆失败,账户或者密码错误!");
} else if (exception instanceof DisabledException) {
map.put("msg", "登陆失败,账户被禁用!");
} else if (exception instanceof AccountExpiredException) {
map.put("msg", "登陆失败,账户已过期!");
} else if (exception instanceof CredentialsExpiredException) {
map.put("msg", "登陆失败,密码已过期!");
} else {
map.put("msg", "登陆失败!");
}
ObjectMapper mapper = new ObjectMapper();
String jsonStr = mapper.writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(jsonStr);
}
}
3,修改。。。。。。。。
3,创建自定义权限不足处理器
@Component
public class AccessFailureHandler implements AccessDeniedHandler {
//指定向客户端响应的json串
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
Map<String, Object> map = new HashMap<>();
map.put("code", 403);
map.put("message", "权限不足");
//将Map转成json串
String jsonStr = JSON.toJSONString(map);
//将json串响应出去
response.setContentType("application/json;charset=utf-8");//设置响应正文类型是json
response.getWriter().write(jsonStr);//写出json串
}
}
7,SpringSecurity认证流程
1, SpringSecurity认证授权介绍
SpringSecurity主要是通过Filter或AOP等技术来实现的。当初始化SpringSecurity时,其会创建一个名为springSecurityFilterChain的过滤器,类型为FilterChainProxy,所有请求都会先经过此过滤器;但FilterChainProxy只是一个代理,真正起作用的是它所代理的过滤器链中的各个过滤器;这些过滤器作为bean被Spring管理,它们是SpringSecurity核心,各有各的职责,但它们并不直接处理用户的认证和授权,而是把交给了认证管理器(AuthenticationManager)和决策(权限)管理器(AccessDecisionManager)进行处理。
2,SpringSecurity认证流程
3,自定义 UserDetailsService从数据库查询用户
1,SpringSecurity的配置类
@Configuration
//开启方法级别的授权
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//向SpringSecurity配置类注入认证成功处理器和认证失败处理器
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
//向SpringSecurity配置类注入拒绝访问处理器
@Autowired
private AccessDeniedHandler accessDeniedHandler;
//以匿名内部类形式向IOC容器中添加一个自定义的UserDetailsService的bean对象
@Bean
public UserDetailsService userDetailsService(){
return new UserDetailsService() {
/*
重写loadUserByUsername()方法从数据库中根据用户名查询用户,参数
username为用户录入的用户名,返回值UserDetails对象;
*/
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
System.out.println(username);
/*
从数据库中根据用户名查询用户......
*/
//将查询到的用户信息封装到UserDetails并返回(静态数据模拟)
UserDetails userDetails = User
.withUsername(username)
.password(passwordEncoder().encode("123"))
.roles("CEO","CTO","CTO")
.authorities("sys:add", "sys:delete", "sys:update", "sys:query")
.build();
return userDetails;
}
};
}
//配置用户信息
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*
//模拟内存用户 -- 在内存配置两个用户
auth.inMemoryAuthentication()
.withUser("jake")//用户名
.password(passwordEncoder().encode("123"))//密码
.roles("CEO","CTO","CTO")//角色
.authorities("sys:add", "sys:delete", "sys:update", "sys:query")//权限
.and()
.withUser("smith")//用户名
.password(passwordEncoder().encode("456"))//密码
.roles("CMO")//角色
.authorities("sys:query");//权限
*/
//使用我们自定义的UserDetailsService从数据库查询用户进行认证
auth.userDetailsService(userDetailsService());
}
//配置加密器,即配置PasswordEncoder的bean对象
@Bean
public PasswordEncoder passwordEncoder(){
//返回PasswordEncoder的实现BCryptPasswordEncoder的实例
return new BCryptPasswordEncoder();
}
/*
通过对http请求的拦截验证来配置用户、角色、权限,实现url访问授权:
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/*
这是前后端不分离的处理方式:
http.formLogin()//提供一个登录表单
.successForwardUrl("/welcome")//登录成功转发请求 /项目/welcome
.failureForwardUrl("/login");//登录失败转发请求 /项目/login,回到登录表单
*/
http.formLogin()
.successHandler(authenticationSuccessHandler)//登录成功,使用认证成功处理器处理
.failureHandler(authenticationFailureHandler);//登录失败,使用认证失败处理器处理
//应用我们自定义的拒绝访问处理器
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
/*
url请求授权:
*/
/*
http.authorizeRequests()
//具有sys:add权限可发出/项目/add的请求
.antMatchers("/add").hasAnyAuthority("sys:add")
//具有sys:delete权限可发出/项目/delete的请求
.antMatchers("/delete").hasAnyAuthority("sys:delete")
//具有sys:update权限可发出/项目/update的请求
.antMatchers("/update").hasAnyAuthority("sys:update")
//具有sys:query权限可发出/项目/query的请求
.antMatchers("/query").hasAnyAuthority("sys:query");
*/
http.authorizeRequests()
//对/项目/free的请求,不受任何限制,直接放行
.antMatchers("/free").permitAll();
http.authorizeRequests()
//其它所有请求,登录后就可以请求
.anyRequest().authenticated();
}
}
3,SpringSecurity集成Thymeleaf
1,设计数据库
2,创建项目环境
1,加入相关依赖
<!--加入SpringSecurity 集成Thymeleaf的依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency>
2,启动类配置自动扫描mapper接口
//指定Mapper接口所在的包,自动扫描Mapper接口
@MapperScan(basePackages = "com.example.mapper")
@SpringBootApplication
public class Demo2Application {
public static void main(String[] args) {
SpringApplication.run(Demo2Application.class, args);
}
}
3,配置application.yml配置文件
服务器端口及项目访问路径
server:
port: 8080
servlet:
context-path: /security01--------------------------
数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_security?serverTimezone=UTC
username: root
password: root---------------------
实体类包别名,mapper映射文件路径,开启驼峰命名,开启日志
mybatis:
type-aliases-package: com.example.pojo
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3,在项目templates下添加模板页面
4,实现登录认证
1,实体类User
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
private Integer uid;
private String uname;
private String upwd;
private String sex;
private String address;
private Integer status;
private List<String> auths;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (CollectionUtils.isEmpty(auths)){
return Collections.EMPTY_LIST;
}
List<GrantedAuthority> grantedAuthorityList=new ArrayList<>();
for (String auth:auths){
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(auth);
grantedAuthorityList.add(grantedAuthority);
}
return grantedAuthorityList;
}
@Override
public String getPassword() {
return upwd;
}
@Override
public String getUsername() {
return uname;
}
@Override
public boolean isAccountNonExpired() {
return status==1;
}
@Override
public boolean isAccountNonLocked() {
return status==1;
}
@Override
public boolean isCredentialsNonExpired() {
return status==1;
}
@Override
public boolean isEnabled() {
return status==1;
}
2,UserMapper接口
public interface UserMapper {
public User queryUserByName(String username);
public List<String> queryAuthorityById(Integer uid);
}
3,UserMapper.xml映射文件
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.mapper.UserMapper">
<select id="queryUserByName" resultType="user">
select * from sys_user where uname = #{username}
</select><select id="queryAuthorityById" resultType="string">
select DISTINCT t3.percode from sys_usr_role t1,sys_role_permission t2,sys_permission t3 where t1.roleid = t2.roleid and t2.perid = t3.perid and t1.usrid = #{uid}
</select>
</mapper>
4,LoginUserDetailService业务类
根据用户名查询用户,再根据用户id查询权限,将权限封装到user并返回
@Service
public class LoginUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.queryUserByName(username);
if (!ObjectUtils.isEmpty(user)){
List<String> auths = userMapper.queryAuthorityById(user.getUid());
if (!CollectionUtils.isEmpty(auths)){
user.setAuths(auths);
}
}
return user;
}
}
5,SpringSecurity的配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
protected LoginUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/doLogin")
.usernameParameter("uname")
.passwordParameter("upwd")
.successForwardUrl("/toIndex")
.failureForwardUrl("/toLogin")
.permitAll();
http.logout()
.logoutUrl("/toLogout")//指定登出请求的url接口
.logoutSuccessUrl("/toLogin")//指定登出成功后访问的url接口 -- 访问/toLogin接口到达login.html
.permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.cors().disable();
http.cors().disable();
//关闭CSRF验证
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
6,LoginController控制器
@Controller
public class LoginController {
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
@RequestMapping("/toIndex")
public String toIndex(){
return "index";
}
}
5,实现授权
1,SpringSecurity的配置类开启方法级别的授权
@EnableGlobalMethodSecurity(prePostEnabled = true)
2,编写UserController控制器
@Controller
public class UserController {
/*
处理/项目/toAdd的请求,返回逻辑视图名add,即到达模板页面add.html;
具有user:add权限的用户可发出/项目/toAdd的请求,即执行该方法;
*/
@RequestMapping("/toAdd")
@PreAuthorize("hasAnyAuthority('user:add')")
public String toAdd(){
return "add";
}
/*
处理/项目/toDelete的请求,返回逻辑视图名delete,即到达模板页面delete.html;
具有user:delete权限的用户可发出/项目/toDelete的请求,即执行该方法;
*/
@RequestMapping("/toDelete")
@PreAuthorize("hasAnyAuthority('user:delete')")
public String toDelete(){
return "delete";
}
/*
处理/项目/toUpdate的请求,返回逻辑视图名update,即到达模板页面update.html;
具有user:update权限的用户可发出/项目/toUpdate的请求,即执行该方法;
*/
@RequestMapping("/toUpdate")
@PreAuthorize("hasAnyAuthority('user:update')")
public String toUpdate(){
return "update";
}
/*
处理/项目/toQuery的请求,返回逻辑视图名query,即到达模板页面query.html;
具有user:query权限的用户可发出/项目/toQuery的请求,即执行该方法;
*/
@RequestMapping("/toQuery")
@PreAuthorize("hasAnyAuthority('user:query')")
public String toQuery(){
return "query";
}
/*
处理/项目/toExport的请求,返回逻辑视图名export,即到达模板页面export.html;
具有user:export权限的用户可发出/项目/toExport的请求,即执行该方法;
*/
@RequestMapping("/toExport")
@PreAuthorize("hasAnyAuthority('user:export')")
public String toExport(){
return "export";
}
}
3,修改index.html
<!DOCTYPE html>
<!--引入thymeleaf和thymeleaf对SpringSecurity的支持-->
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<!--具有user:query权限的用户才会展示该链接-->
<a href="toQuery" sec:authorize="hasAuthority('user:query')">查询用户</a>
<br>
<!--具有user:add权限的用户才会展示该链接-->
<a href="toAdd" sec:authorize="hasAuthority('user:add')">添加用户</a>
<br>
<!--具有user:update权限的用户才会展示该链接-->
<a href="toUpdate" sec:authorize="hasAuthority('user:update')">更新用户</a>
<br>
<!--具有user:delete权限的用户才会展示该链接-->
<a href="toDelete" sec:authorize="hasAuthority('user:delete')">删除用户</a>
<br>
<!--具有user:export权限的用户才会展示该链接-->
<a href="toExport" sec:authorize="hasAuthority('user:export')">导出用户</a>
<br><br>
<h3><a href="toLogout">退出</a></h3>
</body>
</html>
6,SpringSecurity图片验证码校验
1,引入hutool的依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.11</version>
</dependency>
2,修改login.html页面
<tr>
<td>验证码:</td>
<td>
<input type="text" name="code">
<img src="codeImage" style="height:25px;cursor:pointer;"onclick="this.src=this.src">
</td>
</tr>
3,创建CodeController
@Controller
public class CodeController {
//处理/项目/codeImage的请求,向客户端响应验证码图片
@RequestMapping("/codeImage")
public void codeImage(HttpSession session, HttpServletResponse response)
throws IOException {
//生成验证码图片 --- 宽 高 字符个数 干扰线个数
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(250, 100, 4, 20);
//LineCaptcha对象存储到session中
session.setAttribute("SESSION_CODE", captcha);
//将验证码图片响应给img标签
captcha.write(response.getOutputStream());
}
}
4,修改SecurityConfig配置类中对/codeImage的请求放行
http.authorizeRequests().antMatchers("/codeImage").permitAll();
5,验证码校验
1,创建自定义异常类CodeException
public class CodeException extends RuntimeException {
public CodeException() {
}
public CodeException(String message) {
super(message);
}
}
2,创建自定义验证码过滤器CodeFilter
package com.mmy.filter;
/*
1)class CodeFilter extends OncePerRequestFilter
继承SpringWeb提供的OncePerRequestFilter类定义过滤器,就无须再注册
过滤器,而且每次请求都会执行过滤器;
2)@Component将自定义的过滤器加入容器;
*/
@Component
public class CodeFilter extends OncePerRequestFilter {
//过滤器拦截到请求执行的方法
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp,
FilterChain chain)
throws ServletException, IOException {
//拿到请求的资源路径
String path = req.getServletPath();
//判断,如果是/doLogin的请求即登录就校验验证码,不是登录就放行
if(path.equals("/doLogin")){
//获取录入的验证码
String code = req.getParameter("code");
/*
判断,如果录入的验证码不为空且和session中保存的验证码相同则放行,否则向
request中存储自定义的CodeException异常对象,并转发到处理异常的控制器;
*/
if(code!=null&&!code.isEmpty()){
HttpSession session = req.getSession();
LineCaptcha captcha=(LineCaptcha)session.getAttribute("SESSION_CODE");
if(captcha.verify(code)){
//放行
chain.doFilter(req, resp);
return;
}
}
//向request中存储自定义的CodeException异常对象,并转发请求/项目/codeException
req.setAttribute("codeException", new CodeException("验证码错误"));
req.getRequestDispatcher("/codeException").forward(req, resp);
return;
}
//放行
chain.doFilter(req, resp);
}
}
3,修改SpringSecurity配置类
注入自定义的校验验证码的过滤器
@Autowired
private CodeFilter codeFilter;
向过滤器链中用户认证过滤器之前添加自定义的校验验证码的过滤器
http.addFilterBefore(codeFilter, UsernamePasswordAuthenticationFilter.class);
对/项目/codeImage和/项目/codeException的请求都直接放行
http.authorizeRequests()
.antMatchers("/codeImage","/codeException")
.permitAll();
4,修改CodeController控制器
//处理/项目/codeException的请求,抛出request中存储的自定义的CodeException异常
@RequestMapping("/codeException")
public void codeException(HttpServletRequest request){
throw (CodeException)request.getAttribute("codeException");
}
//当控制器抛出CodeException异常,向model中存储异常信息,并回到模板页面login.html
@ExceptionHandler(CodeException.class)
public String codeExceptionHandler(Exception e, Model model){
model.addAttribute("errMsg", e.getMessage());
return "login";
5,修改login.html
头引入thymeleaf
<html lang="en" xmlns:th="http://www.thymeleaf.org">
验证码框后加入异常信息
<span th:text="${errMsg}" style="color:red;"></span>
7,记住我功能实现
当用户发起登录请求时,会通过过滤器UsernamePasswordAuthenticationFilter进行用户认证;认证成功之后,SpringSecurity会使用RememberMeService为用户生成Token(令牌,一个随机字符串)并将它写入浏览器 的Cookie中,同时,RememberMeService内部的TokenRepository还会将Token持久化(存入数据库一份);当用户再次访问服务器资源的时候,首先会经过过滤器RememberMeAuthenticationFiler,该过滤器会读取当前请求携带的 Cookie中的Token,然后再去数据库中查找是否有相应的Token,如果有,则再通过UserDetailsService获取用户信息。
1,修改login.html
<tr>
<td></td>
<td>
<input type="checkbox" name="remember-me">记住我
</td>
</tr>
2,修改SpringSecurity的配置类
注入数据源
@Autowired
private DataSource dataSource;
配置TokenRepository的bean对象
@Bean
public PersistentTokenRepository tokenRepository(){
//创建的是PersistentTokenRepository的实现类JdbcTokenRepositoryImpl的对象
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
//给TokenRepository注入数据源(因为其要操作数据库存储Token)
tokenRepository.setDataSource(dataSource);
//第一次实现记住我功能时会自动创建存储Token的表,二次之后要改为false不再建表
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
配置记住我功能
http.rememberMe()
//记住我checkbox的name属性值
.rememberMeParameter("remember-me")
//指定用于持久化Token的TokenRepository
.tokenRepository(tokenRepository())
//指定获取用户信息的UserDetailsService
.userDetailsService(myUserDetailsService);
4,JWT
1,JWT的简介
Json Web Token(JWT),是一种⽤于通信双⽅之间传递信息的简洁的、安全的声明规范,适用于分布式站点的单点登录(SSO)场景。JWT是一种用于传递Token的解决方案,而且可以无需持久化Token实现跨域认证。
2,JWT的原理
服务端认证通过以后,会生成一个JSON对象,并加上签名,发回给客户端
客户端与服务端通信的时候,都要发回这个JSON对象给服务端
服务端完全只靠这个JSON对象认定用户身份
3, JWT的数据结构
由三部分组成,用.隔开,Header(头部),Payload(载体),Signature(签名)
1,Header(头部)
Header部分是一个JSON对象,描述了JWT的元数据,通常是下面的样子:
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名用的算法(algorithm),默认是HMAC SHA256(写成HS256);
typ属性表示这个令牌(token)的类型(type),JWT统一写为JWT;
最后,将上面的JSON对象使用 Base64URL编码转成字符串。
2, Payload(载体)
Payload部分也是一个JSON对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段供选用:
sub(subject):主题
iat(issuedAt):签发时间
exp(expiresAt):过期时间
iss(issuer):签发人
aud(audience):受众
nbf(notBefore):生效时间
jti(jwtId):编号
除了官方字段,还可以在这个部分定义私有字段,例如:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息(密码,手机号等)放在这个部分;但也是可以加密的,生成原始Token以后,可以用密钥再加密一次。
这个JSON对象也要使用Base64URL编码转成字符串。
3, Signature(签名)
Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。
然后,使用Header里面指定的算法(HMAC SHA256),按照下面的公式产生签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
4, JWT的传递方式
客户端每次请求服务端,请求中都要带上这个JWT串。可以把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在请求头Authorization里面,或放在POST请求的数据体里面。
5,JWT的API
1,创建maven项目,引入jwt依赖
<!--jwt依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
2,创建main方法类测试
public class Demo {
public static void main(String[] args) {
/*
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJ1c2VyLWluZm8iLCJuYW1lIjoibGlzaSIsImlkIjoxMDEsImV4cCI6MTY1NjgyMzU1
MywiaWF0IjoxNjU2ODIzNDMzfQ.
IghmOPUVvQCGAtXUZaA4udE-4A7h_RCr3mDkID6qlm0
*/
System.out.println(createToken());
parseToken(createToken());//101 lisi
}
//生成jwt加密串 --- token
public static String createToken(){
//创建Map<String,Object>封装Header信息
Map<String,Object> header = new HashMap<>();
header.put("alg", "HS256");//签名用的算法
header.put("typ", "JWT");//token类型,统一为JWT
//当前时间 -- 签发时间
Date createTime = new Date();
//两小时后的时间 -- 过期时间
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 2);
Date expireTime = calendar.getTime();
String token = JWT.create()
//设置Header
.withHeader(header)
//设置Payload(载体)
.withSubject("user-info")//设置主题
.withIssuedAt(createTime)//设置签发时间
.withExpiresAt(expireTime)//设置过期时间
.withClaim("id", 101)//设置私有字段id
.withClaim("name", "lisi")//设置私有字段name
//设置Signature(签名)
.sign(Algorithm.HMAC256("mmy-123"));//指定秘钥为mmy-123
return token;
}
//解析jwt加密串
public static void parseToken(String token){
//指定秘钥拿到JWT解析器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("mmy-123")).build();
//传递jwt加密串(token),拿到解析后的jwt串
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//从解析后的jwt串中获取相关信息
Claim idClaim = decodedJWT.getClaim("id");//用户id--私有字段id
System.out.println(idClaim.asInt());
Claim nameClaim = decodedJWT.getClaim("name");//用户名--私有字段name
System.out.println(nameClaim.asString());
}
}