认证流程在这里插入图片描述

1.数据库表

user表:
在这里插入图片描述

CREATE TABLE `user_role` (
  `id` char(20) COLLATE utf8_unicode_ci NOT NULL,
  `user_id` char(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `role_id` char(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

roler表:
在这里插入图片描述

CREATE TABLE `role` (
  `id` char(20) COLLATE utf8_unicode_ci NOT NULL,
  `role_name` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

** permission表:**
在这里插入图片描述

CREATE TABLE `permission` (
  `id` char(20) COLLATE utf8_unicode_ci NOT NULL,
  `name` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  `path` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
  `component` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

user_role表:
在这里插入图片描述

CREATE TABLE `user_role` (
  `id` char(20) COLLATE utf8_unicode_ci NOT NULL,
  `user_id` char(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `role_id` char(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

role_permission表:
在这里插入图片描述

CREATE TABLE `role_permission` (
  `id` char(20) COLLATE utf8_unicode_ci NOT NULL,
  `role_id` char(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `permission_id` char(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

2.实现UserDetails接口

封装了用户信息以及相关的权限信息

public class MyUserDetails implements UserDetails {
    private User user;

    private List<String> permissionList;

    public MyUserDetails() {
    }

    public MyUserDetails(User user, List<String> permissionList) {
        this.user = user;
        this.permissionList = permissionList;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public List<String> getPermissionList() {
        return permissionList;
    }

    public void setPermissionList(List<String> permissionList) {
        this.permissionList = permissionList;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        for (String permission : permissionList) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
            grantedAuthorityList.add(simpleGrantedAuthority);
        }
        return grantedAuthorityList;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.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;
    }
}

3.实现UserDetailsService接口

主要是通过loadUserByUsername()方法去数据库查询用户的信息和对应的权限

@Component
public class MyUserDetailsServiceImpl implements MyUserDetailsService {

    @Autowired
    IUserService userService;
    @Autowired
    IPermissionService permissionService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
        if(user == null){
            return null;
        }
        MyUserDetails userDetails = new MyUserDetails();
        userDetails.setUser(user);

        List<Permission> permissions =  permissionService.getByUserId(user.getId());
        List<String> list = new ArrayList<>();
        for (Permission permission : permissions) {
            String name = permission.getName();
            list.add(name);
        }
        userDetails.setPermissionList(list);

        return userDetails;
    }
}

4.添加密码加密处理器类DefaultPasswordEncoder

/**
 * 加密处理工具类
 */
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
    public DefaultPasswordEncoder(){
        this(-1);
    }

    public DefaultPasswordEncoder(int strength){

    }

    @Override
    public String encode(CharSequence charSequence) {
        return MD5.encrypt(charSequence.toString());
    }

    @Override
    public boolean matches(CharSequence charSequence, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
    }

    public static void main(String[] args) {
        DefaultPasswordEncoder defaultPasswordEncoder = new DefaultPasswordEncoder();
        String encode = defaultPasswordEncoder.encode("456");
        System.out.println(encode);
    }
}

MD5工具类:

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加密出错!!+" + e);
        }
    }

    public static void main(String[] args) {
        System.out.println(MD5.encrypt("111111"));
    }

}

5.添加过滤器和处理器

(1)登录认证过滤器UsernamePasswordAuthenticationFilter

  • 在方法attemptAuthentication()里,获取前端传来的username和password,并将其封装成一个未认证UsernamePasswordAuthenticationToken,将这个类传给AuthenticationManager()进行验证。
  • 验证通过后调用方法successfulAuthentication(),根据用户信息生成对应的token,将用户对应的权限信息存储在redis服务器,并将这个token返回给前端
  • 验证失败调用unsuccessfulAuthentication()方法,返回错误信息
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = this.obtainUsername(request);
        username = username != null ? username : "";
        username = username.trim();
        String pass_word = this.obtainPassword(request);
        pass_word = pass_word != null ? pass_word : "";
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, pass_word);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        MyUserDetails userDetails = (MyUserDetails) authResult.getPrincipal();

        String token = tokenManager.createToken(userDetails.getUser().getUsername());

        redisTemplate.opsForValue().set(userDetails.getUser().getUsername(), userDetails.getPermissionList());

        ResponseUtil.out(response, ResponseVo.success(token));

    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        ResponseUtil.out(response, ResponseVo.fail(ReturnCode.INVALID_TOKEN.getCode(),"密码错误"));
    }
}

jwt生成Token

@Component
public class TokenManager {
    //token有效时长
    private long tokenEcpiration = 24 * 60 * 60 * 1000;
    //编码密钥
    private String tokenSignKey = "123456";

    //1.根据用户名生成token
    public String createToken(String username){
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenEcpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP).compact();
        return token;
    }

    //2.根据token字符得到用户时间
    public String getUserInfoFromToken(String token){
        String usernfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return usernfo;
    }

    public void removeToken(String token){

    }

    public static void main(String[] args) {
        TokenManager tokenManager = new TokenManager();
        String token = tokenManager.createToken("wangwang");
        System.out.println(token);
        String userInfoFromToken = tokenManager.getUserInfoFromToken(token);
        System.out.println(userInfoFromToken);
    }


}

(2)登出处理器LogoutHandler

当用户发起\logout请求时,触发TokenLoginoutHandler,在redis服务器删除对应的用户权限信息

public class TokenLoginoutHandler implements LogoutHandler {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenLoginoutHandler(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){
            String username = tokenManager.getUserInfoFromToken(token);
            redisTemplate.delete(username);
            ResponseUtil.out(response, ResponseVo.success("退出成功"));
        }
    }
}

(3)授权处理器BasicAuthenticationFilter

用户请求时,会携带token信息,经过该拦截器时会调用doFilterInternal()这个方法,将从redis获取权限列表,并封装已认证的UsernamePasswordAuthenticationToken,并将这个类放到SecurityContextHolder.getContext()

public class TokenAuthenticationFilter extends BasicAuthenticationFilter {

    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        super(authenticationManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取当前认证成功的用户权限信息
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(authentication != null){
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request,response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
        String token = request.getHeader("token");
        if(token != null){
            String username = tokenManager.getUserInfoFromToken(token);
            //从redis获取权限列表
            List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            for(String permissionValue : permissionValueList){
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permissionValue);
                authorities.add(simpleGrantedAuthority);
            }
            return new UsernamePasswordAuthenticationToken(username, token, authorities);
        }
        return null;

    }
}

(4)授权失败处理AuthenticationEntryPoint

public class UnauthEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseUtil.out(response, ResponseVo.fail(ReturnCode.RC403.getCode(),"无权限"));
    }
}

6.添加配置器类

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限注解配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private MyUserDetailsService myUserDetailsService;
    private DefaultPasswordEncoder passwordEncoder;
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    @Autowired
    public SecurityConfig(MyUserDetailsService myUserDetailsService, DefaultPasswordEncoder passwordEncoder, TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.myUserDetailsService = myUserDetailsService;
        this.passwordEncoder = passwordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthEntryPoint())
                .and().csrf().disable() //关闭csrf保护
                .authorizeRequests()
                .anyRequest().authenticated()//所有的请求都需要权限认证
                .and().logout().logoutUrl("/logout")//配置登出请求
                //配置登出对应的处理器
                .addLogoutHandler(new TokenLoginoutHandler(tokenManager, redisTemplate))
                .and()
                //添加登录认证过滤器
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
                //添加授权过滤器
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate));
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用自己的UserDetailsService和passwordEncoder
        auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder);
    }

}

7.在方法上设置访问权限

@RestController
public class TestController {
    @PreAuthorize("hasAuthority('admin.queryAll')")
    @GetMapping("/hello")
    String hello(){
        return "hello,security!";
    }
}

代码:
https://gitee.com/wangwang_520666/spring-security/tree/master/demo2

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐