执行流程图
2. 引入依赖
<dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> dependency> <dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency> <dependency> <groupId>org.apache.commonsgroupId> <artifactId>commons-lang3artifactId> dependency> <dependency> <groupId>com.maydaygroupId> <artifactId>mayday_commonartifactId> <version>1.0-SNAPSHOTversion> dependency> <dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> dependency> <dependency> <groupId>tk.mybatisgroupId> <artifactId>mapper-spring-boot-starterartifactId> <version>2.1.5version> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-redisartifactId> dependency>
3. 配置文件
server: port: 9003spring: application: name: mayday-auth datasource: driverClassName: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: admin redis: host: 127.0.0.1eureka: client: serviceUrl: defaultZone: http://127.0.0.1:9001/eureka/ instance: prefer-ip-address: truemybatis: type-aliases-package: com.mayday.auth.pojo configuration: map-underscore-to-camel-case: truersa: key: pubKeyFile: D:/document/key/key_rsa.pub priKeyFile: D:/document/key/key_rsa allowPaths: - /auth/login/** - /auth/code/**
4. 创建pojo类
@Data@Entity@Table(name = "tb_user")public class User implements UserDetails { @Id @KeySql(useGeneratedKeys = true) private Long id; private String username; private String password; private Boolean status; private String phone; @Transient private String checkCode; private List roles; @Override @JsonIgnore public Collection extends GrantedAuthority> getAuthorities() { return roles.stream().map(Role::getPermissions).findAny().get(); } @Override @JsonIgnore public boolean isAccountNonExpired() { return true; } @Override @JsonIgnore public boolean isAccountNonLocked() { return true; } @Override @JsonIgnore public boolean isCredentialsNonExpired() { return true; } @Override @JsonIgnore public boolean isEnabled() { return true; }}
@Data@Entity@Table(name = "tb_role")public class Role { @Id @KeySql(useGeneratedKeys = true) private Long id; @Column(name = "role_name") private String roleName; @Column(name = "role_desc") private String roleDesc; private List permissions;}
@Data@Entity@Table(name = "tb_permission")public class Permission implements GrantedAuthority { @Id @KeySql(useGeneratedKeys = true) private Long id; @Column(name = "permission_name") private String permissionName; @Column(name = "permission_desc") private String permissionDesc; @Override @JsonIgnore public String getAuthority() { return permissionName; }}
5.创建dao和service层
user接口public interface UserMapper extends Mapper<User> {}role接口public interface RoleMapper extends Mapper<Role> { @Select("select r.id,r.role_name,r.role_desc from tb_role r left join tb_user_role ur on r.id = ur.role_id where ur.user_id = #{userId}") ListselectAllByUserId(Long userId);}permission接口public interface PermissionMapper extends Mapper<Permission> { @Select("select p.id,p.permission_name,p.permission_desc from tb_permission p left join tb_role_permission rp on p.id = rp.permission_id where rp.role_id = #{roleId}") ListselectAllByRoleId(Long roleId);}
@Servicepublic class UserService implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private PermissionMapper permissionMapper; @Autowired private RoleMapper roleMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = new User(); user.setUsername(username); User currentUser = userMapper.selectOne(user); if (currentUser == null) { return null; } //设置用户的角色 List roles = roleMapper.selectAllByUserId(currentUser.getId()); if (!CollectionUtils.isEmpty(roles)) { for (Role role : roles) { //查询每个角色对应的权限 List permissions = permissionMapper.selectAllByRoleId(role.getId()); if (!CollectionUtils.isEmpty(permissions)) { role.setPermissions(permissions); } } currentUser.setRoles(roles); } return currentUser; }}
6. 创建Controller层
@RestController@RequestMapping("/auth")public class UserController { @Autowired private UserService userService; @Autowired private StringRedisTemplate redisTemplate; @Autowired private RsaKeyProperties rsaKeyProperties; private static final String REDIS_PREFIX = "mayday:login:code"; private static final int OUT_TIME = 30; private static final String TOKEN_PREFIX = "Bearer "; /** * 生成验证码 * * @param response * @param request * @throws IOException */ @GetMapping("/code") public void captcha(HttpServletResponse response, HttpServletRequest request) throws IOException { response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); // 生成图片验证码 BufferedImage image = CaptchaUtil.createImage(); // 生成文字验证码 String randomText = CaptchaUtil.drawRandomText(image); // 保存到验证码到 redis 有效期两分钟 redisTemplate.opsForValue().set(REDIS_PREFIX, randomText.toLowerCase(), 1, TimeUnit.MINUTES); ServletOutputStream out = response.getOutputStream(); ImageIO.write(image, "jpeg", out); } /** * 登录 * * @param user * @return */ @PostMapping("/login") public Result login(@RequestBody User user) { //检查验证码是否正确 String checkCode = user.getCheckCode(); String redisCode = redisTemplate.opsForValue().get(REDIS_PREFIX); if (StringUtils.isEmpty(checkCode) || !redisCode.equals(checkCode)) { return new Result(false, StatusCode.CHECK_CODE_NOT_EXIT.getCode(), StatusCode.CHECK_CODE_NOT_EXIT.getMsg()); } //验证登录 User currentUser = (User) userService.loadUserByUsername(user.getUsername()); if (currentUser == null) { return new Result(false, StatusCode.USERNAME_NOT_FOUND.getCode(), StatusCode.USERNAME_NOT_FOUND.getMsg()); } //检验密码是否输入正确 if (new BCryptPasswordEncoder().matches(user.getPassword(), currentUser.getPassword())) { return new Result(false, StatusCode.LOGIN_ERROR.getCode(), StatusCode.LOGIN_ERROR.getMsg()); } String token = TOKEN_PREFIX + JwtUtils.generateTokenExploreInMinutes(currentUser, rsaKeyProperties.getPrivateKey(), OUT_TIME); return new Result(true, StatusCode.OK.getCode(), StatusCode.OK.getMsg(), token); } /** * 权限测试接口 * @return */ @GetMapping("test") @PreAuthorize("hasAnyAuthority('product_list')") public String test() { return "test success"; }}
7. 创建配置类
@Data@ConfigurationProperties("rsa")public class AllowPathsProperties { private List allowPaths;}@Data@ConfigurationProperties("rsa")public class AllowPathsProperties { private List allowPaths;}
8. 创建Spring Security配置类
@Slf4j@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private RsaKeyProperties rsaKeyProperties; @Autowired private AllowPathsProperties allowPathsProperties; /** * 解决无法直接注入AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 装载BCrypt密码编码器 密码加密 * * @return */ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // 设置UserDetailsService 使用BCrypt进行密码的hash auth.userDetailsService(userService).passwordEncoder(this.passwordEncoder()); } @Override public void configure(HttpSecurity http) throws Exception { http // 由于使用的是JWT,我们这里不需要csrf .csrf().disable() // 过滤请求 .authorizeRequests() // 对于登录login 图标 要允许匿名访问 .antMatchers(allowPathsProperties.getAllowPaths().stream().toArray(String[]::new)).anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable() .and().addFilter(new LoginFilter(super.authenticationManager(), rsaKeyProperties)) //基于token,不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) ; }}
9.创建过滤器类
public class LoginFilter extends BasicAuthenticationFilter { private RsaKeyProperties rsaKeyProperties; public LoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) { super(authenticationManager); this.rsaKeyProperties = rsaKeyProperties; } @Override public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { //如果携带错误的token,则给用户提示请登录! chain.doFilter(request, response); response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); PrintWriter out = response.getWriter(); Map resultMap = new HashMap(); resultMap.put("code", HttpServletResponse.SC_FORBIDDEN); resultMap.put("msg", "请登录!"); out.write(new ObjectMapper().writeValueAsString(resultMap)); out.flush(); out.close(); } else { //如果携带了正确格式的token要先得到token String token = header.replace("Bearer ", ""); //验证token是否正确 Payload payload = JwtUtils.genInfoFromToken(token, rsaKeyProperties.getPublicKey(), User.class); User user = payload.getUserInfo(); if (user != null) { UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authResult); chain.doFilter(request, response); } } }}
10. 创建自定义异常处理类
/** * 认证失败处理类 返回401 * * @author lihaodong */@Slf4j@Componentpublic class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //如果携带错误的token,则给用户提示请登录! PrintWriter out = response.getWriter(); Result result = new Result(false, StatusCode.UN_AUTHORIZATION); out.write(new ObjectMapper().writeValueAsString(result)); out.flush(); out.close(); }}/** * 权限不足认证 */@Componentpublic class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); //如果携带错误的token,则给用户提示请登录! PrintWriter out = httpServletResponse.getWriter(); Result result = new Result(false, StatusCode.ACCESS_ERROR); out.write(new ObjectMapper().writeValueAsString(result)); out.flush(); out.close(); }} @Autowired private AuthenticationEntryPointImpl authenticationEntryPoint; @Autowired private AccessDeniedHandlerImpl accessDeniedHandler; @Override public void configure(HttpSecurity http) throws Exception { http // 由于使用的是JWT,我们这里不需要csrf .csrf().disable() //认证失败处理类 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler) }}
11.测试结果