一个简单的security安全登录示例
- 配置环境
- 添加pom依赖坐标
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> </dependencies>
- application.yml文件配置
server: port: 9999 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db_security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root main: allow-bean-definition-overriding: true redis: port: 6379 host: 127.0.0.1 logging: level: root: INFO org.springframework.web.servlet.DispatcherServlet: DEBUG org.springframework.cloud.sleuth: DEBUG
- 数据库就一张用户表(简单登录:用户表)
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for tb_user -- ---------------------------- DROP TABLE IF EXISTS `tb_user`; CREATE TABLE `tb_user` ( `id` int(11) NOT NULL, `account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `role` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of tb_user -- ---------------------------- INSERT INTO `tb_user` VALUES (1, '123456', 'admin', '$2a$10$gGKeupjLdvbaoYdAlz1MfuiT/4llYvGFU2QeQ..CQIhCXyqXekn5.', 'test'); INSERT INTO `tb_user` VALUES (2, '789456', 'zhangsan', '$2a$10$gGKeupjLdvbaoYdAlz1MfuiT/4llYvGFU2QeQ..CQIhCXyqXekn5.', NULL); SET FOREIGN_KEY_CHECKS = 1;
- 编写统一返回类
@Data @Accessors(chain = true) public class R { //成功状态位 private Boolean success; //响应消息 private String message; //响应码 private Integer code; //响应数据 private Object data; private static final String DEFAULT_SUCCESS_MESSAGE = "OK"; private static final Integer DEFAULT_SUCCESS_CODE = 20000; private static final String DEFAULT_FALL_MESSAGE = "ERROR"; private static final Integer DEFAULT_FALL_CODE = 50000; private R(Boolean success, String message, Integer code, Object data) { this.success = success; this.message = message; this.code = code; this.data = data; } public static R success(){ R r = new R(true,DEFAULT_SUCCESS_MESSAGE,DEFAULT_SUCCESS_CODE,null); return r; } public static R success(String message){ R r = new R(true,message,DEFAULT_SUCCESS_CODE,null); return r; } public static R success(Object data){ R r = new R(true,DEFAULT_SUCCESS_MESSAGE,DEFAULT_SUCCESS_CODE,data); return r; } public static R success(String message,Object data){ R r = new R(true,message,DEFAULT_SUCCESS_CODE,data); return r; } public static R fail(){ R r = new R(false,DEFAULT_FALL_MESSAGE,DEFAULT_FALL_CODE,null); return r; } public static R fail(String message){ R r = new R(false,message,DEFAULT_FALL_CODE,null); return r; } public static R fail(String message,Integer code){ R r = new R(false,message,code,null); return r; } }
- redis配置
@Configuration public class RedisConfig { /** * retemplate相关配置 * @param factory * @return */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); // 配置连接工厂 template.setConnectionFactory(factory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); // 值采用json序列化 template.setValueSerializer(jacksonSeial); //使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); // 设置hash key 和value序列化模式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(jacksonSeial); template.afterPropertiesSet(); return template; } }
- 添加pom依赖坐标
- 创建实体类,实现接口UserDetails,实现获取权限方法
@Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) public class UserBean implements UserDetails { private String account; private String username; private String password; private String role; @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null) { return authorities; } authorities = new ArrayList<>(); if(null!=role){ SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role); authorities.add(simpleGrantedAuthority); } return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
- 创建mapper接口,编写sql语句
@Mapper public interface UserBeanMapper { //根据账号查询用户信息 @Select("select * from tb_user where account=#{account}") UserBean findUserById(@Param("account")String account); }
- 创建service层登录接口
public interface UserBeanService { R login(String account,String password); }
- 实现service层接口
@Service public class UserServiceImpl implements UserBeanService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisTemplate<String,Object> redisTemplate; @Override public R login(String account, String password) { //创建用户身份验证 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(account, password); Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken); if(null==authenticate){ return R.fail("账户名或密码错误"); } //以hash方式存储redis中 redisTemplate.boundHashOps(account).put(account, JSON.toJSONString(authenticate.getPrincipal())); //设置redis过期时间 redisTemplate.boundHashOps(account).expire(100000, TimeUnit.MILLISECONDS); return R.success(JwtUtil.createJWT(account)); } }
- 创建controller层
@RestController public class UserBeanController { @Autowired private UserBeanService userBeanService; //登录 @PostMapping("/login") public R login(@RequestParam("account") String account, @RequestParam("password") String password){ return userBeanService.login(account,password); } //权限测试 @GetMapping("/test") @PreAuthorize("hasAuthority('test')") public R test(){ return R.success("这是一条提示消息","test"); } }
security相关代码配置
- 这里使用jwt令牌,创建jwt工具类
/** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "mingwen"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @param ttlMillis token超时时间 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis=JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("sanyue") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
- 创建jwt过滤器
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String,Object> redisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { filterChain.doFilter(request, response); return; } String account = null; //校验token try { Claims claims = JwtUtil.parseJWT(token); account = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("非法token"); } //判断redis该用户是否超时存在 if(Boolean.FALSE.equals(redisTemplate.boundHashOps(account).hasKey(account))){ throw new RuntimeException("用户登录超时请重新登录"); } //取出该用户相关信息 同时延长redis存储时间 String redisUserObj = redisTemplate.boundHashOps(account).get(account).toString(); redisTemplate.boundHashOps(account).expire(100000, TimeUnit.MILLISECONDS); UserBean userBean = JSON.parseObject(redisUserObj, UserBean.class); //将用户信息,及权限交给security UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userBean,null,userBean.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } }
- 继承security适配器,配置security
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; //密码校验规则 @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); //把token校验过滤器添加到过滤器链中 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/login/**", "/logout/**" ); } }
- 实现接口UserDetailsService 并实现方法,走我们自己的业务逻辑
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserBeanMapper userBeanMapper; @Override public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException { return userBeanMapper.findUserById(account); } }
启动项目
- 启动类
@SpringBootApplication public class SecurityApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(SecurityApplication.class, args); BCryptPasswordEncoder bean = run.getBean(BCryptPasswordEncoder.class); System.out.println(bean.encode("123456")); } }
- 测试数据
- admin登录
- admin访问对应权限方法
- zhangsan登录
- zhangsan访问权限方法
写的不怎么样,希望对大家有用,希望大家多多指点~~~