15 Spring Security integrated thymeleaf
This project is based on springsecurity-12-database-authorization-method Copy
springsecurity-12-database-authorization-method and renamed to springsecurity-13-thymeleaf
15.1 Add thymeleaf dependency
|
org.springframework.boot
spring-boot-starter-thymeleaf
15.2 Modify application.yml
Add thymeleaf configuration
| spring:
thymeleaf:
cache: false # Do not use cache check-template: true # Check if the thymeleaf template exists |
---|
15.3 idea add thymeleaf template
【File】—》【Settings...】
The template name is thymeleaf, the extension name is html, and the specific content is as follows:
|
#[[ END ENDEND]]#
A brief description:
#[[ T i t l e Title Title ]]# # [ [ END ENDEND ]]# The function of these two places is that when you create a new template page, in |
---|
15.4 New LoginController
| @Controller
@RequestMapping(“/login”)
public class LoginController {
_/**
* Jump to login page
*/
_@RequestMapping(“/toLogin”)
public String toLogin(){
return “login”;
}
} |
---|
15.5 Create thymeleaf file login.html
Create login.html under templates, using the template to create
|
log in page
username:
password:
Log in
15.7 Modify the security configuration file WebSecurityConfig
The modification is as follows:
| @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 {
//Set the login method
http.formLogin()//Login method using username and password.usernameParameter
("uname") //The name of the user name of the page form
.passwordParameter("pwd")//The name of the password of the page form
.loginPage(“/login/toLogin”) //Define the address of the login page
yourself.loginProcessingUrl(“/login/doLogin”)//Configure the login url
.successForwardUrl(“/index/toIndex”) //The page that jumps after successful login.failureForwardUrl
(“/login/toLogin”)//The page that jumps after login failure.permitAll
(); //Release the url related to login, Don't forget to write this
//Configure the exit method
http.logout()
.logoutUrl(“/logout”)
.logoutSuccessUrl(“/login/toLogin”)
.permitAll();/Release and exit related urls, don’t forget to write this
//Configure the matching rules of the url intercepted by the path
http.authorizeRequests()
//Any path requires authentication before accessing.anyRequest
().authenticated();
// Disable csrf cross-site request attack You can use the postman tool to test later, pay attention to disable csrf
http.csrf().disable();
}
} |
---|
15.8 Create IndexController
| @Controller
@RequestMapping(“/index”)
public class IndexController {
_/**
* Enter the homepage after successful login
*/
_@RequestMapping(“/toIndex”)
public String toIndex(){
return “main”;
}
} |
---|
15.9 Create thymeleaf file main.html
Create main.html under templates
|
System Home
quit
15.10 Modifying the Studentcontroller
The modification is as follows:
| @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 Create each page of student management under templates/user
Create export.html
|
System Home-Student Management-Export
Create query.html
|
System Home-Student Management-Query
Create add.html
|
System home page - student management - add
Create update.html
|
System Home-Student Management-Update
Create delete.html
|
System Home-Student Management-Delete
15.12 Create 403 page
Create 403.html under static/error
|
403: You do not have permission to access this page
15.13 Start Test
Note: If there is a 404 problem, generally this problem does not occur
spring-boot-starter-parent org.springframework.boot 2.6.13 |
---|
15.14 When the user does not have a certain permission, the page does not display the button (just look at it)
In the project we created in the last lecture, when the user clicks the link on the page to request to go to the background, it will jump to 403 without permission. If the user does not have permission, the corresponding button will not be displayed. Wouldn’t it be better?
Let's continue with the previous project to transform
and introduce the following dependencies
|
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
Just modify main.html
|
xmlns:sec=“http://www.thymeleaf.org/extras/spring-security”>
System Home
quit
Check the effect after restarting and logging in
16 springsecurity integrated image verification code
In the past, because we wrote our own login method, we could receive the code passed from the page in our own login method, and then compare it with the correct code in the session.
16.1 Overview
In the previous lecture, we integrated thymeleaf to realize the dynamic judgment of whether to display the page link. Then in the actual development, we will encounter the function of verification code, so how to deal with it?
Copy the previous project springsecurity-13-thymeleaf, modify the name to springsecurity-14-captcha
16.2 Principles, Existing Problems, and Solutions
Spring Security's filter chain
We know that Spring Security is done through the filter chain, so its solution is to create a filter and put it in the Security filter chain, and compare the verification code in the custom filter
16.3 Add dependencies (for generating verification codes)
|
cn.hutool
hutool-all
5.3.9
16.4 Add an interface to get the verification code
| @Controller
@Slf4j
public class CaptchaController {
@GetMapping(“/code/image”)
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
//Create a captcha
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha ( 200, 100, 2, 20);
//Put it in the session
//Why do you want to refactor? What is the shortcut key for refactoring?
String captchaCode = circleCaptcha. getCode();
log .info("The generated verification code is: {}", captchaCode);
request.getSession().setAttribute(“LOGIN_CAPTCHA_CODE”,captchaCode);
ImageIO.write(circleCaptcha.getImage(),“JPEG”,response.getOutputStream());
}
} |
---|
16.5 Create captcha filter
| @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("The verification code entered by the user is: {}, the verification code in the session is: {}", enterCaptchaCode, captchaCodeInSession);
//Remove the error message
session.removeAttribute("captchaCodeErrorMsg");
if (!StringUtils.hasText(captchaCodeInSession)) {
session.removeAttribute(“LOGIN_CAPTCHA_CODE”);
}
if (!StringUtils.hasText(enterCaptchaCode) || !StringUtils.hasText(captchaCodeInSession) || !enterCaptchaCode.equalsIgnoreCase(captchaCodeInSession)) {
//Indicates that the verification code is incorrect, return to the login page
session.setAttribute(“captchaCodeErrorMsg”, “The verification code is incorrect”);//Redirect
response.sendRedirect(“/login/toLogin”);
}else{
filterChain.doFilter(request,response);
}
} @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { //If it is not a login request, let it pass without going through the filter return !request.getRequestURI().equals(“/login/doLogin”); }
} |
---|
16.6 Modify WebSecurityConfig (emphasis)
| @EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private ValidateCodeFilter validateCodeFilter;
@Override
_/**
* Security http request configuration
*
* **@param **http
* **@throws **Exception
*/
_@Override
protected void configure(HttpSecurity http) throws Exception { //Set the login method http.formLogin()//Login method using username and password.usernameParameter ("uname") //The name of the user name of the page form
.passwordParameter("pwd")//The name of the password of the page form
.loginPage(“/login/toLogin”) //Define the address of the login page
yourself.loginProcessingUrl(“/login/doLogin”)//Configure the login url
.successForwardUrl(“/index/toIndex”) //The page that jumps after successful login
.failureForwardUrl(“/login/toLogin”)//The page that jumps after login
failure.permitAll(); // Don't forget this
//Configuration Exit method
http.logout()
.logoutUrl(“/logout”)
.logoutSuccessUrl(“/login/toLogin”)
.permitAll();
//Configure the matching rules of the url intercepted by the path, and release the request to obtain the verification code path
http.authorizeRequests().antMatchers(“/code/image”).permitAll()
//Any path requires authentication before accessing.anyRequest
().authenticated();
// Disable csrf cross-site request, be careful not to write it wrong, because the front-end page does not pass token, so
http.csrf().disable() should be disabled;
// Configure a filter to add a verification code before logging in http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class);
}_
_@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
} |
---|
16.7 Modify login.html
Add captcha form elements and images
|
log in page
username:
password:
verification code:
username
Log in
16.8 Test login
Intentionally entered wrong verification code
16.9 Use the debug mode to check the custom filter execution process
17 Base64 and JWT learning
See "base64 and jwt learning document.doc"
18 JWT+Spring Security+redis+mysql to achieve authentication
18.1 New construction
Copy the project springsecurity-12-database-authorization-method, and change the name to
springsecurity-16-jwt-authentication
Note that this project already has authentication functions and method-based authorization functions.
Let's take a look at how to set up authentication and login using jwt.
18.2 Add jwt dependency
|
com.auth0
java-jwt
3.11.0
18.3 Configure keys in application.yml
| jwt:
secretKey: mykey |
---|
18.4 jwt function class
Created under com.powernode.util package
| @Component
@Slf4j
public class JwtUtils {
//Algorithm key
@Value("${jwt.secretKey}")
private String jwtSecretKey;
_/**
* create jwt
*
* **@param **userInfo user information
* **@param **authList user permission list
* **@return **return jwt (JSON WEB TOKEN)
*/
_public String createToken(String userInfo, List authList) {
// Create time
Date currentTime = new Date();
//Expiration time, expires in 5 minutes
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”);
//Create jwt
return JWT.create()
.withHeader(headerClaims) // head.withIssuedAt
(currentTime) //registered claim: issue date, issue date.withExpiresAt
(expireTime) //registered claim expiration time.withIssuer
("thomas") //registered claim, Issuer.withClaim
(“userInfo”, userInfo) //Private claim, you can define it yourself.withClaim
(“authList”, authList) //Private claim, you can customize
it.sign(Algorithm.HMAC256 ( jwtSecretKey )); // Signature , signed using the HS256 algorithm, and using a key
// HS256 is a symmetric algorithm, which means there is only one key, shared between the two parties. Use the same key to generate a signature and verify it. Special attention should be paid to whether the key is kept confidential.
}
_/**
* Verify the signature of jwt, referred to as signature verification
*
* **@param **token jwt that needs to be verified
* **@return **signature verification result
*/
_public boolean verifyToken(String token) {
//Get the signature verification class object
JWTVerifier jwtVerifier = JWT.require (Algorithm.HMAC256 ( jwtSecretKey )).build();
try {
//Verify the signature, if no error is reported, it means that the jwt is legal and has not expired
DecodedJWT decodedJWT = jwtVerifier.verify(token);
return true;
} catch (JWTVerificationException e) {
//If an error is reported, it means that jwt is illegal, or has expired (expired is also illegal)
log .error("signature verification failed: {}", token);
e.printStackTrace();
}
return false;
}
_/**
* Get user id
*
* **@param **token jwt
* **@return **User ID
*/
_public String getUserInfo(String token) {
// Create jwt signature verification object
JWTVerifier jwtVerifier = JWT.require (Algorithm.HMAC256 ( jwtSecretKey )).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//Get the value of userInfo in the payload, and return
return decodedJWT.getClaim(“userInfo”).asString();
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
_/**
* Get User Permissions
*
* **@param **token
* **@return
***/
_public List getUserAuth(String token) {
// Create jwt signature verification object
JWTVerifier jwtVerifier = JWT.require (Algorithm.HMAC256 ( jwtSecretKey )).build();
try {
//验签
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//Get the custom data authList (permission list) in the payload, and return
return decodedJWT.getClaim(“authList”).asList(String.class);
} catch (JWTVerificationException e) {
e.printStackTrace();
}
return null;
}
} |
---|
18.5 Adding a Response Class
com.powernode.vo package
| @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult implements Serializable {
private Integer code; //response code
private String msg; //response message
private Object data; //response object
} |
---|
18.6 Modifying the SecurityUser class
Add a method to get SysUser, which will be used later
| public SysUser getSysUser() {
return sysUser;
} |
---|
18.7 Create a new authentication success handler
| _/**
* Authentication success processor, when the user logs in successfully, this processor will be executed
*/
_@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
//Use this tool class for serialization
@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”);
//Get the authentication user information from the authentication object
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
String userInfo=objectMapper.writeValueAsString(securityUser.getSysUser());
List authorities = (List) securityUser.getAuthorities();
//_This can be changed to stream_List
authList=new ArrayList<>();
for (SimpleGrantedAuthority authority : authorities) {
authList.add(authority.getAuthority());
}//Use stream 1, use lambda expression
List test = authorities.stream().map(
a -> {
return a.getAuthority();
}
).collect(Collectors.toList());
System.out.println("test = " + test);
//Use stream 2
List test111 = authorities.stream().map(SimpleGrantedAuthority::getAuthority).collect(Collectors.toList());
System.out.println ("test111 = " + test111); //
create jwt
String token = jwtUtils.createToken(userInfo,authList);
//Return to the front-end 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 Create a new jwt filter for checking tokens, etc.
New class in com.powernode.filter package
| _/**
* Define a one-time request 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 {
//Get the Authorization in the request header
String authorization = request.getHeader(“Authorization”);
//If Authorization is empty, then the user is not allowed to access, return directly
if (!StringUtils.hasText (authorization)) {
printFront(response, "Not logged in!");
return;
}
//Authorization Remove the Bearer information in the header and get the token value
String jwtToken = authorization.replace("Bearer ", "");
//Verify signature (signature verification for short)
boolean verifyTokenResult = jwtUtils.verifyToken(jwtToken);
//signature verification is unsuccessful
if (!verifyTokenResult) {
printFront(response, "jwtToken has expired");
return;
}
//Get userInfo from payload
String userInfo = jwtUtils.getUserInfo(jwtToken);
//Get the authorization list from the payload
List userAuth = jwtUtils.getUserAuth(jwtToken);
//Create login user
SysUser sysUser = objectMapper.readValue(userInfo, SysUser.class);
SecurityUser securityUser = new SecurityUser(sysUser);
//Set authority
List authList = userAuth.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList ( ));
securityUser.setAuthorityList(authList);
// Create username password token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToke = new UsernamePasswordAuthenticationToken(securityUser
, null, authList);
// Set authentication information through security context
SecurityContextHolder.getContext ().setAuthentication(usernamePasswordAuthenticationToke);
//Continue to visit the corresponding url, etc.
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 { //If it is a login request, let it pass without going through the filter return request.getRequestURI().equals(“/login”); }
} |
---|
18.9 Modify the web security configuration class 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(); //Any request requires authentication (successful login) to access// Disable cross-origin request protection
http.csrf().disable();
// disable session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
} |
---|
18.10 Start Test
Log in to the system first, get the token returned on the page, and then use postman to send the request with the token in the request header.
Use curl to access
curl -H “Authorization:Bearer token” localhost:8080/student/query |
---|
18.11 Post-Test Questions
18.11.1 Issues with Implementing User Logout
Problem: Because JWT is stateless, it cannot be implemented if the exit function is to be implemented.
Solution:
Use redis
steps:
① After successful login, save the generated JWT to redis
key | value |
---|---|
logintoken:jwt | Authentication information authentication |
② When the user logs out, delete the token from redis.
③ Every time the user visits, first check whether the jwt is legal. If it is legal, then take out the logintoken from redis:jwt judges whether the jwt still exists. If it does not exist, it means that the user has If you log out, you will return to not logged in.
18.11.2 Start redis and use client tools to connect to redis
18.11.3 Copy project
Copy springsecurity-16-jwt-authentication into springsecurity-16-jwt-authentication-redis project
18.11.4 Add redis dependency
|
org.springframework.boot
spring-boot-starter-data-redis
18.11.5 Configure redis information
| spring:
** ****redis:
host: 192.168.43.33
port: 6379
database: 0
password: 666666** |
---|
18.11.6 Modify authentication success handler
Add dependency injection
| @Resource
private StringRedisTemplate stringRedisTemplate; |
---|
add in code
stringRedisTemplate.opsForValue().set(“logintoken:”+token,objectMapper.writeValueAsString(authentication),30, TimeUnit.MINUTES); |
---|
18.11.7 New user logout success handler
| _/**
* Exit success handler, after the user exits successfully, execute this handler
*/
_@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
//Use the object of this tool class for serialization
@Resource
private ObjectMapper objectMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// Get Authorization information from the request header
String authorization = request.getHeader("Authorization");
//If the authorization information is empty, return to the front end
if(null==authorization){
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json;charset=utf-8”);
HttpResult httpResult=HttpResult.builder ().code(-1).msg("token cannot be empty").build() ;
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(httpResult));
writer.flush();
return;
}
//If the Authorization information is not empty, remove the Bearer string in the header
String token = authorization.replace("Bearer ", "");
//Delete token in redis, this is the key point
stringRedisTemplate.delete("logintoken:"+token);
response.setCharacterEncoding(“UTF-8”);
response.setContentType(“application/json;charset=utf-8”);
HttpResult httpResult=HttpResult.builder ().code(200).msg("Exit successful").build() ;
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(httpResult));
writer.flush();
}
} |
---|
Configure the user to successfully exit the processor
Modify WebSecurityConfig
dependency injection
| @Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler; |
---|
add code
http.logout().logoutSuccessHandler(myLogoutSuccessHandler);http.csrf().disable(); //Note: Disable cross-domain request protection or logout cannot be accessed |
---|
18.11.8 Modify jwtcheckfilter
Add dependency injection
| @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 |
---|