When I develop with Spring, I usually use the AOP + JWT pattern to verify caller identity. A few days ago, I came into contact with an open source mall source code (Github address), which uses Spring Security + JWT to verify permissions. But the source code only realized a simple user name password authentication, about the authority of the skipped. Having read about Spring Security before but not actually used it, I took the opportunity to integrate spring Security JWT. Github source code address

Spring Security is a Web security framework based on Spring. Generally speaking, the security of Web applications consists of user authentication and user authorization. User authentication is commonly used to verify user names and passwords. User authorization refers to checking whether a user has permission to invoke a resource.

User authentication

For user authentication, we usually need to implement our own PasswordEncoder UserDetail classes. PasswordEncoder mainly implements password encryption and password comparison (login user password and database stored password). The PasswordEncoder code I implemented is as follows

package com.lichaobao.springsecurityjwt.component;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;

/ * * *@author lichaobao
 * @date 2018/12/22
 * @QQ1527563274 * /
public class MyPasswordEncoder implements PasswordEncoder {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyPasswordEncoder.class);

    /** * Custom password encryption (for example, this code does not encrypt the password, just return the old password) *@paramCharSequence specifies the encrypted password *@returnEncrypted password */
    @Override
    public String encode(CharSequence charSequence) {
        LOGGER.info("now encode password :{}",charSequence.toString());
        return charSequence.toString();
    }

    /** * Compares the encrypted password with the database password *@paramCharSequence Specifies the password that the user logs in to@paramS Password stored in the database *@returnTrue Matches false Does not match */
    @Override
    public boolean matches(CharSequence charSequence, String s) {
        LOGGER.info("matchs charSequence :{} and password :{}",charSequence,s);
        returnencode(charSequence).equals(s); }}Copy the code

The UserDetails class encapsulates user information, including basic user information and permission information. The default generation class for UserDetails is UserDetailsService. This interface provides the loadUserByUsername(String username) method UserDetailService source code as follows

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
Copy the code

In this example, we modify the UserdetailsServices code in WebSecurityConfigAdapter as follows

    / * * * in the code and complete user query of basic information such as user name password authorization after encapsulation Return entry for userDetailsService. * this method loadUserByUsername (String username) *@return UserDetail
     */
    @Bean
    @Override
    protected UserDetailsService userDetailsService(a) {
        return username ->{
            if(users.containsKey(username)){
                return new MyUserDetails(username,users.get(username),permissions.get(username));
            }
            throw new UsernameNotFoundException("User name error");
        };
    }
Copy the code

Permission to verify

Custom permission validation is implemented through the custom AccesDecisionVoter class. The key code is as follows

/ * * *@author lichaobao
 * @date 2018/12/22
 * @QQ1527563274 * /
public class RoleBasedVotor implements AccessDecisionVoter {
    private static final Logger LOGGER = LoggerFactory.getLogger(RoleBasedVotor.class);
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;// Cannot directly return false otherwise the authentication fails
    }

    /** * Main validation logic * ROLE_ANONYMOUS stands for all access this is automatically generated by Spring Security and can be customized *@paramAuthentication User information *@paramO you can get the URL * from here@paramIn this example, we use collection * because we use THE URL as the authentication basis@returnACCESS_DENIED(-1) No permission ACCESS_GRANTED (1) Yes */
    @Override
    public int vote(Authentication authentication, Object o, Collection collection) {
        FilterInvocation fi = (FilterInvocation) o;
        String url = fi.getRequestUrl();
        LOGGER.info("url :{}",url);
        if(authentication == null) {return ACCESS_DENIED;
        }
        Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
        Iterator iterator = authorities.iterator();
        while (iterator.hasNext()){
            GrantedAuthority ga = (GrantedAuthority) iterator.next();
            LOGGER.info(ga.getAuthority());
            if(equalsurl(url,ga.getAuthority())||"ROLE_ANONYMOUS".equals(ga.getAuthority())){
                returnACCESS_GRANTED; }}return ACCESS_DENIED;
    }

    @Override
    public boolean supports(Class aClass) {
        return true;// Cannot directly return false otherwise the authentication fails
    }

    /** * Get user permission information *@param authentication
     * @return* /
    Collection<? extends GrantedAuthority> extractAuthorities(
            Authentication authentication) {
        LOGGER.info("extractAuthorites:{}",authentication.getAuthorities());
        return authentication.getAuthorities();
    }

    /** * Comparison permission Permission /** Indicates that all of the following levels can be accessed /* Indicates that the following levels can be accessed. If the user permission is /test/**, the user can access /test/a /test/b/c /test/b/c cannot access *@paramUrl Indicates the url to be accessed@paramUrlpermission Indicates the permission *@return boolean
     */
     static boolean equalsurl(String url,String urlpermission) {
        url = url.startsWith("/")? url.substring(1):url;
        urlpermission = urlpermission.startsWith("/")? urlpermission.substring(1):urlpermission;
        if("* *".equals(urlpermission)){
            return true;
        }else if("*".equals(urlpermission)){
            return url.split("/").length == 1;
        }
        else if(urlpermission.endsWith("/ * *")){
            String afterUrl =  urlpermission.substring(0,urlpermission.length()-3);
            return url.startsWith(afterUrl);
        }else if(urlpermission.endsWith("/ *")){
            String afterUrl = urlpermission.substring(0,urlpermission.length()-2);
            String[] urlPiece = url.split("/");
            return url.startsWith(afterUrl)&&urlPiece.length == 2;
        }
        returnurl.equals(urlpermission); }}Copy the code

Verify landing

On logic in the code, we need to login interface receives the user name, password generation UsernamePasswordAuthenticationToken a bridge for the validation. Note That the password must be encrypted according to the encryption method in the PasswordEncoder or other self-implemented encryption method. Ensure that the encrypted password matches the password in the database. This is then verified by calling the Authenticate method in authenticationManager. The specific code is as follows

@Service
public class SignServiceImpl implements SignService {
    private static final Logger LOGGER = LoggerFactory.getLogger(SignService.class);
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    UserDetailsService userDetailsService;
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    PasswordEncoder passwordEncoder;

    /** * logon error@paramUsername username *@param"Password," password *@return String token
     */
    @Override
    public String login(String username, String password) {
        String token = null;
        /** * encapsulation note password encryption */
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username,passwordEncoder.encode(password));
        try{
            Authentication authentication = authenticationManager.authenticate(authenticationToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            /** * The main logic to load the username and password in the database is the code in UserdetailsServices */
            userDetailsService.loadUserByUsername(username);
            token = jwtUtils.generateToken(username);
        }catch (Exception e){
            e.printStackTrace();
            LOGGER.info("Authentication failed: {}",e.getMessage());
        }
        returntoken; }}Copy the code

Verify login credentials

The specific implementation is to inherit the OncePerRequestFilter method to achieve its own Filter, obtain user information by parsing token, and then compare user permissions. The code is as follows:

/ * * *@author lichaobao
 * @date 2018/12/22
 * @QQ1527563274 * /
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        if(token ! =null && SecurityContextHolder.getContext().getAuthentication() == null){
            String username = jwtUtils.getUserNameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
            LOGGER.info("UserDetails :{},permissions:{}",userDetails.getUsername(),userDetails.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            LOGGER.info("authenticated user :{}",username);
            LOGGER.info("already filter name:{}".super.getAlreadyFilteredAttributeName()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } filterChain.doFilter(request,response); }}Copy the code

Configure security

Concrete implementation for inheritance logic WebSecurityConfigurerAdapter method according to their own needs to be rewritten

/ * * *@author lichaobao
 * @date 2018/12/22
 * @QQ1527563274 * /
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyAccessDeineHandler myAccessDeineHandler;

    @Autowired
    MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    /** * impersonates the database user */
    private static Map<String,String> users;
    /** ** Simulation permission */
    private static Map<String,List<String>> permissions;
    static {
        users = new HashMap<>();
        permissions = new HashMap<>();
        users.put("a"."a");
        String[] aper = new String[]{"/a/**"."/test/all"};
        permissions.put("a",Arrays.asList(aper));
        users.put("b"."b");
        String[] bper = new String[]{"/b/**"."test/all"};
        permissions.put("b",Arrays.asList(bper));
        users.put("admin"."password");
        String[] adminPer = new String[]{"/ * *"};
        permissions.put("admin",Arrays.asList(adminPer));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http.csrf()
                    .disable()// Disable CSRF because using JWT is not required
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)/ / disable the session
                    .and()
                    .authorizeRequests()
                    .accessDecisionManager(accessDecisionManager())// Load your accessDecisionManager using RoleBasedVotor
                    .antMatchers(HttpMethod.GET,
                            "/"."/*.html"."/favicon.ico"."/**/*.html"."/**/*.css"."/**/*.js"."/swagger-resources/**"."/v2/api-docs/**")
                    .permitAll()// Allow access to all interface resources
                    .antMatchers("/login"."/register")
                    .permitAll()ROLE_ANONYMOUS = ROLE_ANONYMOUS = ROLE_ANONYMOUS = ROLE_ANONYMOUS = ROLE_ANONYMOUS = ROLE_ANONYMOUS
                    .antMatchers(HttpMethod.OPTIONS)
                    .permitAll()// Cross-domain requests will have an OPTIONS request all allowed
                    .anyRequest()// Everything else needs to be validated
                    .authenticated();
        /** * Disable cache */
        http.headers().cacheControl();
        /** * Configure the user-defined Filter */
            http.addFilterBefore(jwtAuthenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
        /** * The user name and password are incorrect
        http.exceptionHandling()
                    .accessDeniedHandler(myAccessDeineHandler)
                    .authenticationEntryPoint(myAuthenticationEntryPoint);
    }

    /** * Configure the userDetailsService and passwordEncoder; *@param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    / * * * in the code and complete user query of basic information such as user name password authorization after encapsulation Return entry for userDetailsService. * this method loadUserByUsername (String username) *@return UserDetail
     */
    @Bean
    @Override
    protected UserDetailsService userDetailsService(a) {
        return username ->{
            if(users.containsKey(username)){
                return new MyUserDetails(username,users.get(username),permissions.get(username));
            }
            throw new UsernameNotFoundException("User name error");
        };
    }
    @Bean
    public PasswordEncoder passwordEncoder(a){
        return new MyPasswordEncoder();
    }
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(a){
        return new JwtAuthenticationTokenFilter();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean(a) throws Exception {
        return super.authenticationManagerBean();
    }

    /** * Use the RoleBasedVotor method@return* /
    @Bean
    public AccessDecisionManager accessDecisionManager(a){
        List<AccessDecisionVoter<? extends Object>> decisionVoters
                = Arrays.asList(
                new WebExpressionVoter(),
                new RoleBasedVotor(),
                new AuthenticatedVoter());
        return newUnanimousBased(decisionVoters); }}Copy the code