security作为一个流行的安全框架,很多公司都用其来做web的认证与授权。activiti7 工作流默认使用其的 用户-角色 功能。所以我们必须了解它。
建立web项目
首先建立一个web项目(springboot)同时我们写上一个接口用于测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @RestController public class TestController { private Logger logger = LoggerFactory.getLogger(TestController.class); @GetMapping(value = "/admin/1") public Object admin(){ Map map = new HashMap(); map.put("code",200); map.put("msg","success"); map.put("data","admin"); return map; } @GetMapping("/user/1") public Object user(){ Map map = new HashMap(); map.put("code",200); map.put("msg","success"); map.put("data","user"); return map; } @GetMapping("/free/1") public Object free(){ Map map = new HashMap(); map.put("code",200); map.put("msg","success"); map.put("data","free"); return map; }
@PostMapping("/logout/success") public Object logoutSuccess(){ return "logout success POST"; } @GetMapping("/logout/success") public Object logoutSuccessGet(){ return "logout success Get"; } @PostMapping("/login/error2") public Object loginError(){ return "logoin error"; } @GetMapping("/test") public Object test(){ return "test"; } }
|
启动项目访问我们的接口
(正常返回数据)
添加security依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
|
再次访问接口:
接口提示401 msg显示没有认证 说明security已经其作用了
接着我们再用浏览器看看
http://localhost:8080/admin/1
发现浏览器重定向到一个登录页面
从IDEA控制台我们发现密码巴拉巴拉什么的
然后我们使用 用户名:user 还有控制台的密码登录及可返回我们想要的数据。
从spring security 的文档中也可以找到说明:
当然这种用户名和密码我们也可以自定义:
1 2
| spring.security.user.name=abc spring.security.user.password=123457
|
简单配置与说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| package com.example.springbootactiviti.demo.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity public class SpringSecurityCustomConfig extends WebSecurityConfigurerAdapter {
@Autowired private PasswordEncoder encoding;
@Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder();
}
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth
.inMemoryAuthentication() .withUser("user").password(encoding.encode("123")).roles("USER").and() .withUser("admin").password(encoding.encode("1234")).roles("USER", "ADMIN") .and().withUser("zhnagsan").password(encoding.encode("12345")).roles("LEADER") ; } @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/free/**","/logout/success").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyAuthority("ROLE_USER","ROLE_ADMIN") .antMatchers("/test").hasAnyRole("USER","ADMIN") .anyRequest().authenticated() .and() .formLogin().failureUrl( "/login/error2" ) .and().logout().logoutUrl("/logout/out").logoutSuccessUrl("/logout/success") ; } }
|
在配置权限认证的时候,遵循自上而下的匹配规则。
上面我们的用户是在代码中写死的,而实际项目中我们的用户信息都是在数据库里接下来我们来实现从数据库里读取用户的逻辑。
新建一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| package com.example.springbootactiviti.demo.config;
import com.example.springbootactiviti.demo.model.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList; import java.util.List;
public class SpringDataUserDetailsService implements UserDetailsService {
@Autowired JdbcTemplate jdbcTemplate;
@Autowired private PasswordEncoder passwordEncoder;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<User> userList = jdbcTemplate.query("select * from user where username = ?",new Object[]{username}, new BeanPropertyRowMapper<>(User.class));
if (userList == null) { throw new UsernameNotFoundException("用户不存在"); } userList.forEach(i-> System.out.println(i.toString())); String role = userList.get(0).getRole();
List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
return new org.springframework.security.core.userdetails.User( userList.get(0).getUsername(), userList.get(0).getPassword(), authorities ); } }
|
实体类:
1 2 3 4 5 6 7 8 9 10
| public class User { private String id;
private String username;
private String password;
private String role; }
|
准备数据:
其中user密码明文是123 ,admin 密码明文是1234.这里插入的都是BCrypt加密后的密文。
1 2 3 4 5 6 7 8 9 10 11
| create table user ( id varchar(20) not null, username varchar(50) not null, password varchar(100) not null, role varchar(50) not null ) collate = utf8_bin; INSERT INTO spring_security.user (id, username, password, role) VALUES ('1', 'user', '$2a$10$elDLIbuSf9UZ9XpLr2FVPOgfAQARQURnbymSg7HyxCTW.copZR3Y6', 'USER'); INSERT INTO spring_security.user (id, username, password, role) VALUES ('2', 'admin', '$2a$10$P0mwGYvKDgK5KBr7ybQ7D.GJJ4Ban3wSB/1DvOo17qjktvgH5Pwh6', 'ADMIN');
|
修改配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Autowired private SpringDataUserDetailsService userDetailsService;
@Bean public SpringDataUserDetailsService customUserDetailsService() { return new SpringDataUserDetailsService(); }
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService) ; }
|
好了!如果是一般的单体架构web项目,了解这些基本满足使用了。
由于restful API 与分布式、微服务的流行,现在很多项目都已经实现了前后端的分离。服务端(后端)只提供调用方API,前后端采用token认证的方式进行数据交互。而在token认证方式中 jwt 是比较流行的一种。接下来开始spring security 与 jwt 的整合。
security+JWT
首先简单说下token认证授权的逻辑3步走。
- 用户通过名称和密码访问登录接口 登录成功返回 token
- 用户带上token(一般存放在head中) 访问 资源地址 服务端拦截请求解析token 如果正确则返回资源
- 用户访问登出接口 服务端清除token
再简单认识下security中的常见的过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| UsernamePasswordAuthenticationFilter
BasicAuthenticationFilter
UsernamePasswordAuthenticationToken
Authentication
UserDetails
UserDetailsService
authenticationManager
|
security 默认的 /login(可配置) 作为登录的url 。当访问该login时。会被
AbstractAuthenticationProcessingFilter拦截后调用UsernamePasswordAuthenticationFilter 的方法得到UsernamePasswordAuthenticationToken做认证。然后调用过滤器链继续过滤
其中UsernamePasswordAuthenticationFilter 里是将读取用户名密码等信息封装成UsernamePasswordAuthenticationToken
当访问需要某项权限的url时(/login 接口不需要权限)会经过BasicAuthenticationFilter过滤器时会进行认证授权。并把认证结果保存在上下文中。
在BasicAuthenticationFilter中这里会在request中解析(通过存在请求头中的内容)出UsernamePasswordAuthenticationToken委托给authenticationManager的authenticate方法进行认证。
在authenticate的具体实现中会取出前面解析出的UsernamePasswordAuthenticationToken 中的用户名
通过UserDetailsService获取到数据库的用户信息(密码)与UsernamePasswordAuthenticationToken 中的密码进行比对。如果发生异常则抛出。
抛出的异常会被BasicAuthenticationFilter捕获 并交给认证失败处理方法(可重写)进行后续处理。
如果没有异常就继续交给过滤器链过滤处理。
还记得上面的4步走么
其中第一步
我们可以手动写一个restful接口用于登录。接口内验证用户名密码无误后通过jwt工具生成一个token返回给客户端。也可以继承自UsernamePasswordAuthenticationFilter 类在请求中拿到用户名密码验证无误后返回token给客户端。(返回客户端前 可把token存入redis 等)
其中第二步
用户访问资源链接时带上token会被BasicAuthenticationFilter拦截。
我们自定义一个类继承与它。把请求头中的token用jwt工具类解析然后封装成UsernamePasswordAuthenticationToken 其它代码不变。(当然如果前面把相关信息存在了redis里这里可直接在redis里取)
其中第三步
我们可以写个restful接口,客户端访问后,我们通过token解析出用户后,清除token(如果是redis做token验证这里清空redis里的token即可,如果不是可通过jwt工具类设置token的时效性使其失效)
补充说明
由于前后端的交互统一的json格式。所以我们需要重写掉security验证失败的默认处理unsuccessfulAuthentication方法。
很多地方是通过AuthenticationFailureHandler类来做默认处理的这里我们继承重写掉这个类即可
补充
1 2 3 4 5 6 7 8 9
| extends UsernamePasswordAuthenticationFilter BasicAuthenticationFilter AbstractSecurityInterceptor OncePerRequestFilter AbstractAuthenticationProcessingFilter implements FilterInvocationSecurityMetadataSource AccessDecisionManager
|