15 SpringSecurity 集成thymeleaf
此项目是在springsecurity-12-database-authorization-method 的基础上进行
复制springsecurity-12-database-authorization-method 并重命名为springsecurity-13-thymeleaf
15.1 添加thymeleaf依赖
|
org.springframework.boot
spring-boot-starter-thymeleaf
15.2 修改application.yml
加入thymeleaf的配置
| spring:
thymeleaf:
cache: false # 不使用缓存check-template: true # 检查thymeleaf模板是否存在 |
---|
15.3 idea 添加thymeleaf模板
【File】—》【Settings…】
模板名称thymeleaf ,扩展名html,具体内容如下:
|
#[[ E N D END END]]#
简要说明:
#[[
T
i
t
l
e
Title
Title]]# #[[
E
N
D
END
END]]# 这两处的作用是,当你新建一个模板页面时,在 |
---|
15.4 新建LoginController
| @Controller
@RequestMapping(“/login”)
public class LoginController {
_/**
* 跳转到登陆页面
*/
_@RequestMapping(“/toLogin”)
public String toLogin(){
return “login”;
}
} |
---|
15.5 创建thymeleaf文件login.html
在templates下面创建login.html,使用模板创建
|
登录页面
用户名:
密码:
登录
15.7 修改安全配置文件WebSecurityConfig
修改后如下:
| @EnableGlobalMethodSecurity(prePostEnabled = true)
//@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置登陆方式
http.formLogin()//使用用户名和密码的登陆方式
.usernameParameter(“uname”) //页面表单的用户名的name
.passwordParameter(“pwd”)//页面表单的密码的name
.loginPage(“/login/toLogin”) //自己定义登陆页面的地址
.loginProcessingUrl(“/login/doLogin”)//配置登陆的url
.successForwardUrl(“/index/toIndex”) //登陆成功跳转的页面
.failureForwardUrl(“/login/toLogin”)//登陆失败跳转的页面
.permitAll(); //放行和登陆有关的url,别忘了写这个
//配置退出方式
http.logout()
.logoutUrl(“/logout”)
.logoutSuccessUrl(“/login/toLogin”)
.permitAll();/放行和退出有关的url,别忘了写这个
//配置路径拦截 的url的匹配规则
http.authorizeRequests()
//任何路径要求必须认证之后才能访问
.anyRequest().authenticated();
// 禁用csrf跨站请求攻击 后面可以使用postman工具测试,注意要禁用csrf
http.csrf().disable();
}
} |
---|
15.8 创建IndexController
| @Controller
@RequestMapping(“/index”)
public class IndexController {
_/**
* 登录成功后进入主页
*/
_@RequestMapping(“/toIndex”)
public String toIndex(){
return “main”;
}
} |
---|
15.9 创建thymeleaf文件main.html
在templates下面创建main.html
|
系统首页
查询学生
添加学生
更新学生
删除学生
导出学生
退出
15.10 修改Studentcontroller
修改后如下:
| @Controller
@Slf4j
@RequestMapping(“/student”)
public class StudentController {
@GetMapping(“/query”)
@PreAuthorize(“hasAuthority(‘student:query’)”)
public String queryInfo(){
return “user/query”;
}
@GetMapping(“/add”)
@PreAuthorize(“hasAuthority(‘student:add’)”)
public String addInfo(){
return “user/add”;
}
@GetMapping(“/update”)
@PreAuthorize(“hasAuthority(‘student:update’)”)
public String updateInfo(){
return “user/update”;
}
@GetMapping(“/delete”)
@PreAuthorize(“hasAuthority(‘student:delete’)”)
public String deleteInfo(){
return “user/delete”;
}
@GetMapping(“/export”)
@PreAuthorize(“hasAuthority(‘student:export’)”)
public String exportInfo(){
return “/user/export”;
}
} |
---|
15.11 在templates/user下面创建学生管理的各个页面
创建export.html
|
系统首页-学生管理-导出
返回
创建query.html
|
系统首页-学生管理-查询
返回
创建add.html
|
系统首页-学生管理-新增
返回
创建update.html
|
系统首页-学生管理-更新
返回
创建delete.html
|
系统首页-学生管理-删除
返回
15.12 创建403页面
在static/error下面创建403.html
|
403:你没有权限访问此页面
去首页
15.13 启动测试
注意:如果出现404问题,一般不出现这个问题
spring-boot-starter-parent org.springframework.boot 2.6.13 |
---|
15.14 当用户没有某权限时,页面不展示该按钮(简单看下即可)
上一讲里面我们创建的项目里面是当用户点击页面上的链接请求到后台之后没有权限会跳转到403,那么如果用户没有权限,对应的按钮就不显示出来,这样岂不是更好吗?
我们接着上一个项目来改造
引入下面的依赖
|
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
修改main.html即可
|
xmlns:sec=“http://www.thymeleaf.org/extras/spring-security”>
系统首页
查询用户
添加用户
更新用户
删除用户
导出用户
退出
重新启动登录后查看效果
16 springsecurity 集成图片验证码
以前因为我们自己写登陆的方法可以在自己的登陆方法里面去接收页面传过来的code,再和session里面正确的code进行比较 。
16.1 概述
上一讲里面我们集成了thymeleaf实现在页面链接的动态判断是否显示,那么在实际开发中,我们会遇到有验证码的功能,那么如何处理呢?
复制上一个工程springsecurity-13-thymeleaf ,修改名字为springsecurity-14-captcha
16.2 原理、存在问题、解决思路
Springsecurity的过滤器链
我们知道Spring Security是通过过滤器链来完成了,所以它的解决方案是创建一个过滤器放到Security的过滤器链中,在自定义的过滤器中比较验证码
16.3 添加依赖(用于生成验证码)
|
cn.hutool
hutool-all
5.3.9
16.4 添加一个获取验证码的接口
| @Controller
@Slf4j
public class CaptchaController {
@GetMapping(“/code/image”)
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
//创建一个验证码
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);
//放到session中
// 为什么要重构?重构的快捷键是啥?
String captchaCode=circleCaptcha.getCode();
log.info(“生成的验证码为:{}”,captchaCode);
request.getSession().setAttribute(“LOGIN_CAPTCHA_CODE”,captchaCode);
ImageIO.write(circleCaptcha.getImage(),“JPEG”,response.getOutputStream());
}
} |
---|
16.5 创建验证码过滤器
| @Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
validateCode(request, response,filterChain);
}
// 验证码校验
private void validateCode(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws IOException, ServletException {
String enterCaptchaCode = request.getParameter(“code”);
HttpSession session = request.getSession();
String captchaCodeInSession = (String) session.getAttribute(“LOGIN_CAPTCHA_CODE”);
log.info(“用户输入的验证码为:{},session中的验证码为:{}”,enterCaptchaCode,captchaCodeInSession);
//移除错误信息
session.removeAttribute(“captchaCodeErrorMsg”);
if (!StringUtils.hasText(captchaCodeInSession)) {
session.removeAttribute(“LOGIN_CAPTCHA_CODE”);
}
if (!StringUtils.hasText(enterCaptchaCode) || !StringUtils.hasText(captchaCodeInSession) || !enterCaptchaCode.equalsIgnoreCase(captchaCodeInSession)) {
//说明验证码不正确,返回登陆页面
session.setAttribute(“captchaCodeErrorMsg”, “验证码不正确”);//重定向
response.sendRedirect(“/login/toLogin”);
}else{
filterChain.doFilter(request,response);
}
} @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { //如果不是登陆请求,直接放行,不走过滤器 return !request.getRequestURI().equals(“/login/doLogin”); }
} |
---|
16.6 修改WebSecurityConfig(重点)
| @EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private ValidateCodeFilter validateCodeFilter;
@Override
_/**
* Security的http请求配置
*
* **@param **http
* **@throws **Exception
*/
_@Override
protected void configure(HttpSecurity http) throws Exception {
//设置登陆方式
http.formLogin()//使用用户名和密码的登陆方式
.usernameParameter(“uname”) //页面表单的用户名的name
.passwordParameter(“pwd”)//页面表单的密码的name
.loginPage(“/login/toLogin”) //自己定义登陆页面的地址
.loginProcessingUrl(“/login/doLogin”)//配置登陆的url
.successForwardUrl(“/index/toIndex”) //登陆成功跳转的页面
.failureForwardUrl(“/login/toLogin”)//登陆失败跳转的页面
.permitAll(); // 这个不要忘了
//配置退出方式
http.logout()
.logoutUrl(“/logout”)
.logoutSuccessUrl(“/login/toLogin”)
.permitAll();
//配置路径拦截 的url的匹配规则 ,放行请求获取验证码的路径
http.authorizeRequests().antMatchers(“/code/image”).permitAll()
//任何路径要求必须认证之后才能访问
.anyRequest().authenticated();
// 禁用csrf跨站请求,注意不要写错了,因为前端页面没有传token,所以要禁用
http.csrf().disable();
// 配置登录之前添加一个验证码的过滤器 http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);
}_
_@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
} |
---|
16.7 修改login.html
添加验证码表单元素和图片
|
登录页面
用户名:
密码:
验证码:
username
登录
16.8 测试登录
故意输入错误验证码
16.9 使用debug模式,查看一下自定义过滤器执行流程
17 Base64 和JWT学习
见《base64及jwt学习文档.doc》
18 JWT+Spring Security+redis+mysql 实现认证
18.1 新建工程
复制工程springsecurity-12-database-authorization-method,改名字为
springsecurity-16-jwt-authentication
注意这个工程已经有认证功能和基于方法授权的功能了。
下面咱们看下如何设置使用jwt进行认证登录。
18.2 添加jwt依赖
|
com.auth0
java-jwt
3.11.0
18.3 application.yml 中配置密钥
| jwt:
secretKey: mykey |
---|
18.4 jwt功能类
com.powernode.util包下创建
| @Component
@Slf4j
public class JwtUtils {
//算法密钥
@Value(“${jwt.secretKey}”)
private String jwtSecretKey;
_/**
* 创建jwt
*
* **@param **userInfo 用户信息
* **@param **authList 用户权限列表
* **@return **返回jwt(JSON WEB TOKEN)
*/
_public String createToken(String userInfo, List authList) {
//创建时间
Date currentTime = new Date();
//过期时间,5分钟后过期
Date expireTime = new Date(currentTime.getTime() + (1000 * 60 * 5));
//jwt 的header信息
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put(“type”, “JWT”);
headerClaims.put(“alg”, “HS256”);
//创建jwt
return JWT.create()
.withHeader(headerClaims) // 头部
.withIssuedAt(currentTime) //已注册声明:签发日期,发行日期
.withExpiresAt(expireTime) //已注册声明 过期时间
.withIssuer(“thomas”) //已注册声明,签发人
.withClaim(“userInfo”, userInfo) //私有声明,可以自己定义
.withClaim(“authList”, authList) //私有声明,可以自定义
.sign(Algorithm.HMAC256(jwtSecretKey)); // 签名,使用HS256算法签名,并使用密钥
// HS256是一种对称算法,这意味着只有一个密钥,在双方之间共享。 使用相同的密钥生成签名并对其进行验证。 应特别注意钥匙是否保密。
}
_/**
* 验证jwt的签名,简称验签
*
* **@param **token 需要验签的jwt
* **@return **验签结果
*/
_public boolean verifyToken(String token) {
//获取验签类对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签,如果不报错,则说明jwt是合法的,而且也没有过期
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return true;
} catch (JWTVerificationException e) {
//如果报错说明jwt 为非法的,或者已过期(已过期也属于非法的)
log.error(“验签失败:{}”, token);
e.printStackTrace();
}
return false;
}
_/**
* 获取用户id
*
* **@param **token jwt
* **@return **用户id
*/
_public String getUserInfo(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中userInfo的值,并返回
return decodedJWT.getClaim(“userInfo”).asString();
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
_/**
* 获取用户权限
*
* **@param **token
* **@return
***/
_public List getUserAuth(String token) {
//创建jwt验签对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecretKey)).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//获取payload中的自定义数据authList(权限列表),并返回
return decodedJWT.getClaim(“authList”).asList(String.class);
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
} |
---|
18.5 添加响应类
com.powernode.vo包中
| @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult implements Serializable {
private Integer code; //响应码
private String msg; //响应消息
private Object data; //响应对象
} |
---|
18.6 修改SecurityUser类
加入一个获取SysUser的方法,后面会用到这个方法
| public SysUser getSysUser() {
return sysUser;
} |
---|
18.7 新建认证成功处理器
| _/**
* 认证成功处理器,当用户登录成功后,会执行此处理器
*/
_@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
//使用此工具类进行序列化
@Resource
private ObjectMapper objectMapper;
@Resource
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“text/html;charset=utf-8”);
//从认证对象中获取认证用户信息
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
String userInfo=objectMapper.writeValueAsString(securityUser.getSysUser());
List authorities = (List) securityUser.getAuthorities();
//_这可以改成stream流
_List authList=new ArrayList<>();
for (SimpleGrantedAuthority authority : authorities) {
authList.add(authority.getAuthority());
}//使用stream流1,使用lambda表达式
List test = authorities.stream().map(
a -> {
return a.getAuthority();
}
).collect(Collectors.toList());
System.out.println("test = " + test);
//使用stream流2
List test111 = authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList());
System.out.println("test111 = " + test111);
// 创建jwt
String token = jwtUtils.createToken(userInfo,authList);
//返回给前端token
HttpResult httpResult = HttpResult.builder().code(200).msg(“OK”).data(token).build();
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(httpResult));
writer.flush();
}
} |
---|
18.8 新建jwt过滤器,用于检查token等
com.powernode.filter包中新建类
| _/**
* 定义一次性请求过滤器
*/
_@Component
@Slf4j
public class JwtCheckFilter extends OncePerRequestFilter {
@Resource
private ObjectMapper objectMapper;
@Resource
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头中的Authorization
String authorization = request.getHeader(“Authorization”);
//如果Authorization为空,那么不允许用户访问,直接返回
if (!StringUtils.hasText(authorization)) {
printFront(response, “没有登录!”);
return;
}
//Authorization 去掉头部的Bearer 信息,获取token值
String jwtToken = authorization.replace("Bearer ", “”);
//验证签名(简称验签)
boolean verifyTokenResult = jwtUtils.verifyToken(jwtToken);
//验签不成功
if (!verifyTokenResult) {
printFront(response, “jwtToken 已过期”);
return;
}
//从payload中获取userInfo
String userInfo = jwtUtils.getUserInfo(jwtToken);
//从payload中获取授权列表
List userAuth = jwtUtils.getUserAuth(jwtToken);
//创建登录用户
SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
SecurityUser securityUser = new SecurityUser(sysUser);
//设置权限
List authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
securityUser.setAuthorityList(authList);
//创建用户名密码token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser
, null, authList);
//通过安全上下文设置认证信息
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToke);
//继续访问相应的url等
filterChain.doFilter(request, response);
}
private void printFront(HttpServletResponse response, String message) throws IOException {
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json;charset=utf-8”);
PrintWriter writer = response.getWriter();
HttpResult httpResult =HttpResult.builder().code(-1).msg(message).build();
writer.print(objectMapper.writeValueAsString(httpResult));
writer.flush();
} @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { //如果是登陆请求,直接放行,不走过滤器 return request.getRequestURI().equals(“/login”); }
} |
---|
18.9修改 web安全配置类WebSecurityConfig
| @EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private JwtCheckFilter jwtCheckFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin().successHandler(myAuthenticationSuccessHandler).permitAll();
http.authorizeRequests()
.mvcMatchers(“/student/**”).hasAnyAuthority(“student:query”,“student:update”)
.anyRequest().authenticated(); //任何请求均需要认证(登录成功)才能访问//禁用跨域请求保护
http.csrf().disable();
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
} |
---|
18.10 启动测试
先登录系统,获取页面上返回的token,然后使用postman 在请求头中携带token发送请求即可。
使用curl 访问
curl -H “Authorization:Bearer token” localhost:8080/student/query |
---|
18.11 测试后的问题
18.11.1 实现用户退出的问题
问题:因为JWT无状态,如果要实现退出功能无法实现。
解决办法:
使用redis
步骤:
① 登陆成功之后把生成JWT存到redis中
key | value |
---|---|
logintoken:jwt | 认证信息authentication |
② 用户退出时,从redis中删除该token
③ 用户每次访问时,先校验jwt是否合法,如果合法再从redis里面取出logintoken:jwt判断这个jwt还存不存在,如果不存在就说明用户已经退出来,就返回未登陆。
18.11.2 启动redis并使用客户端工具连接到redis
18.11.3 复制工程
复制springsecurity-16-jwt-authentication 成springsecurity-16-jwt-authentication-redis工程
18.11.4 加入redis依赖
|
org.springframework.boot
spring-boot-starter-data-redis
18.11.5 配置redis信息
| spring:
** ****redis:
host: 192.168.43.33
port: 6379
database: 0
password: 666666** |
---|
18.11.6 修改认证成功处理器
添加依赖注入
| @Resource
private StringRedisTemplate stringRedisTemplate; |
---|
代码中加入
stringRedisTemplate.opsForValue().set(“logintoken:”+token,objectMapper.writeValueAsString(authentication),30, TimeUnit.MINUTES); |
---|
18.11.7 新建用户退出成功处理器
| _/**
* 退出成功处理器,用户退出成功后,执行此处理器
*/
_@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
//使用此工具类的对象进行序列化操作
@Resource
private ObjectMapper objectMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//从请求头中获取Authorization信息
String authorization = request.getHeader(“Authorization”);
//如果授权信息为空,返回前端
if(null==authorization){
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json;charset=utf-8”);
HttpResult httpResult=HttpResult.builder().code(-1).msg(“token不能为空”).build();
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(httpResult));
writer.flush();
return;
}
//如果Authorization信息不为空,去掉头部的Bearer字符串
String token = authorization.replace("Bearer ", “”);
//redis中删除token,这是关键点
stringRedisTemplate.delete(“logintoken:”+token);
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json;charset=utf-8”);
HttpResult httpResult=HttpResult.builder().code(200).msg(“退出成功”).build();
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(httpResult));
writer.flush();
}
} |
---|
配置用户成功退出处理器
修改WebSecurityConfig
依赖注入
| @Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler; |
---|
添加代码
http.logout().logoutSuccessHandler(myLogoutSuccessHandler);http.csrf().disable(); //注意:禁用跨域请求保护 要不然logout不能访问 |
---|
18.11.8 修改jwtcheckfilter
添加依赖注入
| @Resource
private StringRedisTemplate stringRedisTemplate; |
---|
代码中加入
| // 从redis中获取token
String tokenInRedis = stringRedisTemplate.opsForValue().get(“logintoken:” + jwtToken);
if(!StringUtils.hasText(tokenInRedis)){
printFront(response, “用户已退出,请重新登录”);
return;
} |
---|
18.11.9 启动程序并登录测试
登录后查看redis中是否存储了token
18.11.10 使用jwt token访问/student/query
使用postman测试,发现可以正常访问
18.11.11 使用postman 退出系统
注意携带token,才能退出啊
注意:要禁用跨域请求保护,要不然使用postman无法访问logout端点
http.csrf().disable(); //禁用跨域请求保护 |
---|
18.11.12 再次使用token访问/student/query
现象:发现已经不能正常访问了
原因:虽然token本身并没有过期,但是redis中已经删除了该token,所以不能正常访问了
使用curl 访问
curl -H “Authorization:Bearer token” localhost:8080/student/query |
---|