系统的登录认证、用户授权实现
本次从以下几个内容进行分享:
- 使用Jwt生成公钥和私钥
- 根据Jwt令牌实现登录认证
- 完成用户的鉴权和授权
- 自定义项目跳转的登录页面
这是本次实现的流程图:
使用JWT生成公钥和私钥
JWT令牌生成采用非对称加密算法
1、生成密钥证书
下边命令生成密钥证书,采用RSA算法每个证书包含公钥和私钥
keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
Keytool 是一个java提供的证书管理工具
-alias : 密钥的别名
-keyalg :使用的hash算法
-keypass :密钥的访问密码
-keystore :密钥库文件名,xc.keystore保存了生成的证书
-storepass :密钥库的访问密码
查询证书信息:
keytool -list -keystore changgou.jks
2、导出公钥
openssl是一个加解密工具包,这里使用openssl来导出公钥信息。
安装 openssl:http://slproweb.com/products/Win32OpenSSL.html 安装资料目录下的Win64OpenSSL-1_1_1b.exe 配置openssl的path环境变量,
cmd进入changgou.jks文件所在目录执行如下命令:
keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey
公钥:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/ iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv 9QIDAQAB
-----END PUBLIC KEY-----
将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。
根据Jwt令牌实现登录认证
- 准备工作:
-
数据库准备相应的表
-
准备配置类
-
配置文件
-
server:
port: 9200
spring:
application:
name: user-oauth
cloud:
nacos:
discovery:
server-addr: ${ys.nacos.server}
datasource:
url: ${ys.ds.userurl}
driver-class-name: ${ys.ds.driver}
# url: jdbc:mysql://192.168.23.170:3305/yinjoer_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: ${ys.ds.username}
password: ${ys.ds.password}
redis:
host: ${ys.redis}
encrypt:
key-store:
location: classpath:/changgou.jks
secret: changgou
alias: changgou
password: changgou
auth:
ttl: 3600 #token存储到redis的过期时间
clientId: changgou
clientSecret: changgou
cookieDomain: localhost
cookieMaxAge: -1
- 具体实现
1.Service接口
@Component
public interface AuthService {
/**
*
* @param username 用户名
* @param password 用户密码
* @param clientId 客户端Id
* @param clientsecret 客户端密码
* @return
*/
AuthToken login(String username, String password, String clientId, String clientsecret);
}
2.Service实现类
/**
* 认证实现类
* @author ys
*/
@Service
@Transactional
public class AuthServiceImpl implements AuthService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${auth.ttl}")
private Long ttl;
@Override
public AuthToken login(String username, String password, String clientId, String clientsecret) {
ServiceInstance instance = loadBalancerClient.choose("user-oauth");
URI uri = instance.getUri();
String url = uri+"/oauth/token";
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type","password");
body.add("username", username);
body.add("password", password);
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization", this.getHttpBasic(clientId, clientsecret));
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map responseEntityBody = responseEntity.getBody();
if (responseEntityBody == null || responseEntityBody.get("access_token") == null ||
responseEntityBody.get("jti") == null || responseEntityBody.get("refresh_token") == null) {
throw new RuntimeException("申请令牌失败");
}
AuthToken authToken = new AuthToken();
authToken.setAccessToken((String) responseEntityBody.get("access_token"));
authToken.setRefreshToken((String) responseEntityBody.get("refresh_token"));
authToken.setJti((String) responseEntityBody.get("jti"));
stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(), ttl, TimeUnit.SECONDS);
return authToken;
}
private String getHttpBasic(String clientId, String clientsecret) {
String temp = clientId + ":" + clientsecret;
byte[] bytes = Base64Utils.encode(temp.getBytes());
return "Basic " + new String(bytes);
}
}
3.Controller
/**
* 认证控制层
* @author ys
*/
@RestController
@RequestMapping("/oauth")
public class AuthController {
@Autowired
private AuthService authService;
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
@Value("${auth.cookieDomain}")
private String cookieDomain;
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@PostMapping("/login")
public Result login(@RequestParam("username") String username, @RequestParam("password") String password,
HttpServletResponse response, HttpServletRequest request) {
if (StringUtils.isEmpty(username)) {
throw new RuntimeException("用户不存在");
}
if (StringUtils.isEmpty(password)) {
throw new RuntimeException("密码错误");
}
AuthToken token = authService.login(username, password, clientId, clientSecret);
HttpServletResponse servletResponse = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response, cookieDomain, "/", "uid", token.getJti(), cookieMaxAge, false);
return new Result(true, StatusCode.OK, "获取Token成功", token.getJti());
}
}
4.将登录请求放行,在WebSecurityConfig类中configure(),添加放行路径
/***
* 忽略安全拦截的URL
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/login",
"/oauth/logout","/oauth/tologin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");
}
完成用户的授权
OAuth授权模式有四种:
1.授权码模式(Authorization Code)
2.隐式授权模式(Implicit)
3.密码模式(Resource Owner Password Credentials)
4.客户端模式(Client Credentials)
-
新建一个需要授权的服务,添加OAuth的依赖
-
将public.key添加到resource目录下
-
编写配置类
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String PUBLIC_KEY = "public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
return converter;
}
/**
* 获取非对称加密公钥 Key
* @return 公钥 Key
*/
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}
/***
* Http安全配置,对每个到达系统的http请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers(
"/user/add","/user/load/**"). //配置地址放行
permitAll()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
}
访问user服务就需要携带Jwt令牌才可以,不然会出现下面的错误
}
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
在http header中添加 Authorization: Bearer 令牌就可以访问了。
自定义项目跳转的登录页面
- 修改配置类WebSecurityConfig添加登录页面的路径
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic() //启用Http基本身份验证
.and()
.formLogin() //启用表单身份验证
.and()
.authorizeRequests() //限制基于Request请求访问
.anyRequest()
.authenticated(); //其他请求都需要经过验证
http.formLogin().loginPage("/oauth/tologin")
.loginProcessingUrl("/oauth/login");
}
- 放行登录页面和相关的静态资源
/***
* 忽略安全拦截的URL
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/login",
"/oauth/logout","/oauth/tologin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");
}
- 编写登录页面的跳转路径
login:function () {
app.msg="正在登录";
axios.post("/oauth/login?username="+app.username+"&password="+app.password).then(function (response) {
if (response.data.flag){
app.msg="登录成功";
} else{
app.msg="登录失败";
}
})
}
有些代码和配置因为太繁琐我就没写到里面,以上就是本次分享,如果有不恰当的地方,欢迎指正。