1, Introduction to Spring Security
1. Framework introduction
Spring is a very popular and successful Java application development framework. Based on the spring framework, Spring Security provides a set of
A complete solution for Web application security. Generally speaking, the security of Web applications includes user Authentication and user Authentication
User Authorization consists of two parts.
(1) User authentication refers to verifying whether a user is a legal subject in the system, that is, whether the user can access the system. User recognition
The certificate generally requires the user to provide a user name and password. The system completes the authentication process by verifying the user name and password.
(2) User authorization refers to verifying whether a user has permission to perform an operation. In a system, different users have different permissions
different. For example, for a file, some users can only read it, while others can modify it. Generally speaking, the system will
Different users are assigned different roles, and each role corresponds to a series of permissions.
- Spring Security actually uses a filter to filter multiple request paths.
(1) If it is based on Session, spring security will parse the sessionid in the cookie and find the server storage
And then judge whether the current user meets the requirements of the request.
(2) If it is a token, the token is parsed, and then the current request is added to the permission information managed by spring security
That is, spring security is responsible for the value taking and value setting in the above session mode and token mode
2. Realization of authentication and authorization
The implementation idea of authentication and authorization is: Spring security principle (station B) P272 point 13
If there are many modules in the system, each module needs to be authorized and authenticated, so we choose the form of token based authorization and authentication
Certificate, the user successfully authenticates according to the user name and password, and then obtains a series of permission values of the current user role, with the user name as the key and the permission column
The table is stored in the redis cache in the form of value, and a token return is generated according to the user name related information. The browser records the token in the cookie,
Every time the api interface is called, the token is carried into the header request header by default. Spring security parses the header header to obtain the token information and decode it
Analyze the token to obtain the current user name. According to the user name, you can obtain the permission list from redis, so that spring security can judge the current user name
Does the request have access
2, Integrate Spring Security
1. Create spring under common_ Security module
2. In spring_security introduces related dependencies
<dependencies> <dependency> <groupId>com.atguigu</groupId> <artifactId>common_utils</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!-- Spring Security rely on --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency> </dependencies>
3. Code structure description:
ps: the above class name is a random name (only some specific codes need to inherit the spring security interface)
The specific code is as follows (pay attention to the correct guide package)
TokenWebSecurityConfig core configuration class
package com.atguigu.serurity.config; import com.atguigu.serurity.filter.TokenAuthenticationFilter; import com.atguigu.serurity.filter.TokenLoginFilter; import com.atguigu.serurity.security.DefaultPasswordEncoder; import com.atguigu.serurity.security.TokenLogoutHandler; import com.atguigu.serurity.security.TokenManager; import com.atguigu.serurity.security.UnauthorizedEntryPoint; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; /** * <p> * Security Configuration class * </p> * * @author qy * @since 2019-11-18 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // Open permission control annotation public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private UserDetailsService userDetailsService;// For the class written by yourself, refer to the illustration here in the implementation idea of authentication and authorization. The function is to query user information from the database, and then we just give the returned user data information to spring security private TokenManager tokenManager;// Generate token usage private DefaultPasswordEncoder defaultPasswordEncoder;// Password processing private RedisTemplate redisTemplate; @Autowired // Inject into container using construct public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder, TokenManager tokenManager, RedisTemplate redisTemplate) { this.userDetailsService = userDetailsService; this.defaultPasswordEncoder = defaultPasswordEncoder; this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } /** * Core configuration settings * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthorizedEntryPoint()) .and().csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and().logout().logoutUrl("/admin/acl/index/logout")// Just write the exit address and spring security will exit automatically .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and() .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate)) .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic(); } /** * Password processing * @param auth * @throws Exception */ @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder); } /** * Configure which requests are not intercepted (without permission control) * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/api/**", "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ); } }
Entity classes SecurityUser and User
package com.atguigu.serurity.entity; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * <p> * Security authentication user details * </p> * * @author qy * @since 2019-11-08 */ @Data @Slf4j public class SecurityUser implements UserDetails { //Current login user private transient User currentUserInfo; //Current permissions private List<String> permissionValueList; public SecurityUser() { } public SecurityUser(User user) { if (user != null) { this.currentUserInfo = user; } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); for(String permissionValue : permissionValueList) { if(StringUtils.isEmpty(permissionValue)) continue; SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue); authorities.add(authority); } return authorities; } @Override public String getPassword() { return currentUserInfo.getPassword(); } @Override public String getUsername() { return currentUserInfo.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
-
Since the upper class needs the lower class, the classes are created as follows
package com.atguigu.serurity.entity; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; /** * <p> * User entity class * </p> * * @author qy * @since 2019-11-08 */ @Data @ApiModel(description = "User entity class") public class User implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "WeChat openid") private String username; @ApiModelProperty(value = "password") private String password; @ApiModelProperty(value = "nickname") private String nickName; @ApiModelProperty(value = "User Avatar") private String salt; @ApiModelProperty(value = "User signature") private String token; }
The above classes are written in a fixed way (required by spring security)
TokenAuthenticationFilter authorization filter class
package com.atguigu.serurity.filter; import com.atguigu.commonutils.R; import com.atguigu.commonutils.ResponseUtil; import com.atguigu.serurity.security.TokenManager; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.util.StringUtils; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * <p> * Access filter (authorization filter) * </p> * * @author qy * @since 2019-11-08 */ public class TokenAuthenticationFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) { super(authManager); this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { logger.info("================="+req.getRequestURI()); if(req.getRequestURI().indexOf("admin") == -1) { chain.doFilter(req, res); return; } UsernamePasswordAuthenticationToken authentication = null; try { authentication = getAuthentication(req); } catch (Exception e) { ResponseUtil.out(res, R.error()); } if (authentication != null) { SecurityContextHolder.getContext().setAuthentication(authentication); } else { ResponseUtil.out(res, R.error()); } chain.doFilter(req, res); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { // The token is placed in the header, so we get it first String token = request.getHeader("token"); if (token != null && !"".equals(token.trim())) { String userName = tokenManager.getUserFromToken(token); // Judge whether the logged in user has permission to operate these menus (that is, step 6 of the process diagram: Spring security grants permission to the current user to perform corresponding operations) List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName); Collection<GrantedAuthority> authorities = new ArrayList<>(); for(String permissionValue : permissionValueList) { if(StringUtils.isEmpty(permissionValue)) continue; SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue); authorities.add(authority); } if (!StringUtils.isEmpty(userName)) { return new UsernamePasswordAuthenticationToken(userName, token, authorities); } return null; } return null; } }
TokenLoginFilter authentication filter class (Login Class) (authentication (login) is successful before authorization)
package com.atguigu.serurity.filter; import com.atguigu.commonutils.R; import com.atguigu.commonutils.ResponseUtil; import com.atguigu.serurity.entity.SecurityUser; import com.atguigu.serurity.entity.User; import com.atguigu.serurity.security.TokenManager; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; /** * <p> * The login filter inherits the UsernamePasswordAuthenticationFilter and performs login verification on the user name and password * </p> * * @author qy * @since 2019-11-08 */ public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) { this.authenticationManager = authenticationManager; this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; this.setPostOnly(false); // Set the address of the current login request. This address is written casually. Because the login is done by security for us, we only need to write the process of querying the database this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST")); } /** * Get the user name and password for authentication * @param req * @param res * @return * @throws AuthenticationException */ @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { try { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { throw new RuntimeException(e); } } /** * Login successfully executed this method * @param req * @param res * @param chain * @param auth * @throws IOException * @throws ServletException */ @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { SecurityUser user = (SecurityUser) auth.getPrincipal(); String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername()); // After successful login, put the data into redis redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList()); ResponseUtil.out(res, R.ok().data("token", token)); } /** * Login failed. Execute this method * @param request * @param response * @param e * @throws IOException * @throws ServletException */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
And the following four tool classes in the security package and two tool classes in the two value common
package com.atguigu.serurity.security; import com.atguigu.commonutils.MD5; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * <p> * t Processing method type of password * </p> * * @author qy * @since 2019-11-08 */ @Component public class DefaultPasswordEncoder implements PasswordEncoder { public DefaultPasswordEncoder() { this(-1); } /** * @param strength * the log rounds to use, between 4 and 31 */ public DefaultPasswordEncoder(int strength) { } public String encode(CharSequence rawPassword) { return MD5.encrypt(rawPassword.toString()); } public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5.encrypt(rawPassword.toString())); } }
package com.atguigu.serurity.security; import com.atguigu.commonutils.R; import com.atguigu.commonutils.ResponseUtil; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * <p> * Logout business logic class * </p> * * @author qy * @since 2019-11-08 */ public class TokenLogoutHandler implements LogoutHandler { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) { this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String token = request.getHeader("token"); if (token != null) { tokenManager.removeToken(token); //Clear the permission data in the current user cache String userName = tokenManager.getUserFromToken(token); redisTemplate.delete(userName); } ResponseUtil.out(response, R.ok()); } }
package com.atguigu.serurity.security; import io.jsonwebtoken.CompressionCodecs; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.stereotype.Component; import java.util.Date; /** * <p> * token Administration * </p> * * @author qy * @since 2019-11-08 */ @Component public class TokenManager { private long tokenExpiration = 24*60*60*1000; private String tokenSignKey = "123456"; public String createToken(String username) { String token = Jwts.builder().setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact(); return token; } public String getUserFromToken(String token) { String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject(); return user; } public void removeToken(String token) { //jwttoken does not need to be deleted, and the client can throw it away. } }
package com.atguigu.serurity.security; import com.atguigu.commonutils.R; import com.atguigu.commonutils.ResponseUtil; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * <p> * Unauthorized unified processing * </p> * * @author qy * @since 2019-11-08 */ public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
The two in common are as follows
package com.atguigu.commonutils; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public final class MD5 { public static String encrypt(String strSrc) { try { char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; byte[] bytes = strSrc.getBytes(); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(bytes); bytes = md.digest(); int j = bytes.length; char[] chars = new char[j * 2]; int k = 0; for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; chars[k++] = hexChars[b >>> 4 & 0xf]; chars[k++] = hexChars[b & 0xf]; } return new String(chars); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("MD5 Encryption error!!+" + e); } } public static void main(String[] args) { System.out.println(MD5.encrypt("111111")); } }
package com.atguigu.commonutils; import com.atguigu.commonutils.R; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class ResponseUtil { public static void out(HttpServletResponse response, R r) { ObjectMapper mapper = new ObjectMapper(); response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); try { mapper.writeValue(response.getWriter(), r); } catch (IOException e) { e.printStackTrace(); } } }
In addition, JwtUtils tool class is standby
package com.atguigu.commonutils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.Date; /** * @author helen * @since 2019/10/16 */ public class JwtUtils { //constant public static final long EXPIRE = 1000 * 60 * 60 * 24; //token expiration time public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //Secret key //Method of generating token string public static String getJwtToken(String id, String nickname){ String JwtToken = Jwts.builder() .setHeaderParam("typ", "JWT") .setHeaderParam("alg", "HS256") .setSubject("guli-user") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .claim("id", id) //Set the token body part to store user information .claim("nickname", nickname) .signWith(SignatureAlgorithm.HS256, APP_SECRET) .compact(); return JwtToken; } /** * Judge whether the token exists and is valid * @param jwtToken * @return */ public static boolean checkToken(String jwtToken) { if(StringUtils.isEmpty(jwtToken)) return false; try { Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * Judge whether the token exists and is valid * @param request * @return */ public static boolean checkToken(HttpServletRequest request) { try { String jwtToken = request.getHeader("token"); if(StringUtils.isEmpty(jwtToken)) return false; Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * Get the member id according to the token string * @param request * @return */ public static String getMemberIdByJwtToken(HttpServletRequest request) { String jwtToken = request.getHeader("token"); if(StringUtils.isEmpty(jwtToken)) return ""; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); Claims claims = claimsJws.getBody(); return (String)claims.get("id"); } }
4. Facing the framework interface development idea, write your own class to implement the framework interface and execute your own business logic
5,
5.1. Create a custom query user class
(1) In service_ The ACL module is not created because other templates will not be used
UserDetailsServiceImpl authentication user details class
In this class, execute the user query logic and return the user information (securityUser)
package com.atguigu.aclservice.service.impl; import com.atguigu.aclservice.entity.User; import com.atguigu.aclservice.service.PermissionService; import com.atguigu.aclservice.service.UserService; import com.atguigu.serurity.entity.SecurityUser; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; /** * <p> * Custom userDetailsService - authenticated user details * </p> * * @author qy * @since 2019-11-08 */ @Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Autowired private PermissionService permissionService; /*** * Obtain user information according to account number * @param username: * @return: org.springframework.security.core.userdetails.UserDetails */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // Extract user information from the database User user = userService.selectByUsername(username); // Determine whether the user exists if (null == user){ //throw new UsernameNotFoundException("user name does not exist!"); } // Return UserDetails implementation class com.atguigu.serurity.entity.User curUser = new com.atguigu.serurity.entity.User(); BeanUtils.copyProperties(user,curUser); List<String> authorities = permissionService.selectPermissionValueByUserId(user.getId()); SecurityUser securityUser = new SecurityUser(curUser); securityUser.setPermissionValueList(authorities); return securityUser; } }
. . . . . . slightly